tlc-claude-code 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/components/AuditPane.d.ts +30 -0
- package/dashboard/dist/components/AuditPane.js +127 -0
- package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
- package/dashboard/dist/components/AuditPane.test.js +339 -0
- package/dashboard/dist/components/CompliancePane.d.ts +39 -0
- package/dashboard/dist/components/CompliancePane.js +96 -0
- package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
- package/dashboard/dist/components/CompliancePane.test.js +183 -0
- package/dashboard/dist/components/SSOPane.d.ts +36 -0
- package/dashboard/dist/components/SSOPane.js +71 -0
- package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
- package/dashboard/dist/components/SSOPane.test.js +155 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +0 -16
- package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
- package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
- package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
- package/package.json +1 -1
- package/server/lib/access-control-doc.js +541 -0
- package/server/lib/access-control-doc.test.js +672 -0
- package/server/lib/adr-generator.js +423 -0
- package/server/lib/adr-generator.test.js +586 -0
- package/server/lib/agent-progress-monitor.js +223 -0
- package/server/lib/agent-progress-monitor.test.js +202 -0
- package/server/lib/audit-attribution.js +191 -0
- package/server/lib/audit-attribution.test.js +359 -0
- package/server/lib/audit-classifier.js +202 -0
- package/server/lib/audit-classifier.test.js +209 -0
- package/server/lib/audit-command.js +275 -0
- package/server/lib/audit-command.test.js +325 -0
- package/server/lib/audit-exporter.js +380 -0
- package/server/lib/audit-exporter.test.js +464 -0
- package/server/lib/audit-logger.js +236 -0
- package/server/lib/audit-logger.test.js +364 -0
- package/server/lib/audit-query.js +257 -0
- package/server/lib/audit-query.test.js +352 -0
- package/server/lib/audit-storage.js +269 -0
- package/server/lib/audit-storage.test.js +272 -0
- package/server/lib/bulk-repo-init.js +342 -0
- package/server/lib/bulk-repo-init.test.js +388 -0
- package/server/lib/compliance-checklist.js +866 -0
- package/server/lib/compliance-checklist.test.js +476 -0
- package/server/lib/compliance-command.js +616 -0
- package/server/lib/compliance-command.test.js +551 -0
- package/server/lib/compliance-reporter.js +692 -0
- package/server/lib/compliance-reporter.test.js +707 -0
- package/server/lib/data-flow-doc.js +665 -0
- package/server/lib/data-flow-doc.test.js +659 -0
- package/server/lib/ephemeral-storage.js +249 -0
- package/server/lib/ephemeral-storage.test.js +254 -0
- package/server/lib/evidence-collector.js +627 -0
- package/server/lib/evidence-collector.test.js +901 -0
- package/server/lib/flow-diagram-generator.js +474 -0
- package/server/lib/flow-diagram-generator.test.js +446 -0
- package/server/lib/idp-manager.js +626 -0
- package/server/lib/idp-manager.test.js +587 -0
- package/server/lib/memory-exclusion.js +326 -0
- package/server/lib/memory-exclusion.test.js +241 -0
- package/server/lib/mfa-handler.js +452 -0
- package/server/lib/mfa-handler.test.js +490 -0
- package/server/lib/oauth-flow.js +375 -0
- package/server/lib/oauth-flow.test.js +487 -0
- package/server/lib/oauth-registry.js +190 -0
- package/server/lib/oauth-registry.test.js +306 -0
- package/server/lib/readme-generator.js +490 -0
- package/server/lib/readme-generator.test.js +493 -0
- package/server/lib/repo-dependency-tracker.js +261 -0
- package/server/lib/repo-dependency-tracker.test.js +350 -0
- package/server/lib/retention-policy.js +281 -0
- package/server/lib/retention-policy.test.js +486 -0
- package/server/lib/role-mapper.js +236 -0
- package/server/lib/role-mapper.test.js +395 -0
- package/server/lib/saml-provider.js +765 -0
- package/server/lib/saml-provider.test.js +643 -0
- package/server/lib/security-policy-generator.js +682 -0
- package/server/lib/security-policy-generator.test.js +544 -0
- package/server/lib/sensitive-detector.js +112 -0
- package/server/lib/sensitive-detector.test.js +209 -0
- package/server/lib/service-interaction-diagram.js +700 -0
- package/server/lib/service-interaction-diagram.test.js +638 -0
- package/server/lib/service-summary.js +553 -0
- package/server/lib/service-summary.test.js +619 -0
- package/server/lib/session-purge.js +460 -0
- package/server/lib/session-purge.test.js +312 -0
- package/server/lib/sso-command.js +544 -0
- package/server/lib/sso-command.test.js +552 -0
- package/server/lib/sso-session.js +492 -0
- package/server/lib/sso-session.test.js +670 -0
- package/server/lib/workspace-command.js +249 -0
- package/server/lib/workspace-command.test.js +264 -0
- package/server/lib/workspace-config.js +270 -0
- package/server/lib/workspace-config.test.js +312 -0
- package/server/lib/workspace-docs-command.js +547 -0
- package/server/lib/workspace-docs-command.test.js +692 -0
- package/server/lib/workspace-memory.js +451 -0
- package/server/lib/workspace-memory.test.js +403 -0
- package/server/lib/workspace-scanner.js +452 -0
- package/server/lib/workspace-scanner.test.js +677 -0
- package/server/lib/workspace-test-runner.js +315 -0
- package/server/lib/workspace-test-runner.test.js +294 -0
- package/server/lib/zero-retention-command.js +439 -0
- package/server/lib/zero-retention-command.test.js +448 -0
- package/server/lib/zero-retention.js +322 -0
- package/server/lib/zero-retention.test.js +258 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { CompliancePane } from './CompliancePane.js';
|
|
5
|
+
describe('CompliancePane', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
const mockCategories = [
|
|
13
|
+
{ name: 'Security', score: 92, gapCount: 2 },
|
|
14
|
+
{ name: 'Availability', score: 88, gapCount: 3 },
|
|
15
|
+
{ name: 'Confidentiality', score: 95, gapCount: 1 },
|
|
16
|
+
{ name: 'Processing Integrity', score: 78, gapCount: 5 },
|
|
17
|
+
{ name: 'Privacy', score: 85, gapCount: 4 },
|
|
18
|
+
];
|
|
19
|
+
const mockEvidence = [
|
|
20
|
+
{
|
|
21
|
+
id: 'ev-1',
|
|
22
|
+
name: 'Access Control Policy',
|
|
23
|
+
collectedAt: '2024-01-15T10:30:00.000Z',
|
|
24
|
+
status: 'valid',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'ev-2',
|
|
28
|
+
name: 'Encryption Audit',
|
|
29
|
+
collectedAt: '2024-01-14T14:00:00.000Z',
|
|
30
|
+
status: 'valid',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'ev-3',
|
|
34
|
+
name: 'Network Security Scan',
|
|
35
|
+
collectedAt: '2024-01-13T09:00:00.000Z',
|
|
36
|
+
status: 'expired',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
const mockGaps = [
|
|
40
|
+
{ id: 'gap-1', name: 'Missing encryption at rest', severity: 'high', status: 'open' },
|
|
41
|
+
{ id: 'gap-2', name: 'Incomplete backup policy', severity: 'medium', status: 'in-progress' },
|
|
42
|
+
{ id: 'gap-3', name: 'Outdated password policy', severity: 'low', status: 'open' },
|
|
43
|
+
];
|
|
44
|
+
const mockTimeline = [
|
|
45
|
+
{ id: 'tl-1', date: '2024-03-15', event: 'SOC 2 Type II Audit', status: 'upcoming' },
|
|
46
|
+
{ id: 'tl-2', date: '2024-01-10', event: 'Internal Review', status: 'completed' },
|
|
47
|
+
{ id: 'tl-3', date: '2023-09-20', event: 'SOC 2 Type I Certification', status: 'completed' },
|
|
48
|
+
];
|
|
49
|
+
describe('renders compliance score correctly', () => {
|
|
50
|
+
it('renders compliance score correctly', () => {
|
|
51
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 87, riskLevel: "low", categories: [], evidence: [], gaps: [] }));
|
|
52
|
+
const output = lastFrame();
|
|
53
|
+
expect(output).toContain('Compliance');
|
|
54
|
+
expect(output).toContain('87');
|
|
55
|
+
});
|
|
56
|
+
it('shows score with progress bar', () => {
|
|
57
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 75, riskLevel: "medium", categories: [], evidence: [], gaps: [] }));
|
|
58
|
+
const output = lastFrame();
|
|
59
|
+
// Should have some kind of visual indicator
|
|
60
|
+
expect(output).toContain('75');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('renders category breakdown chart', () => {
|
|
64
|
+
it('renders category breakdown chart', () => {
|
|
65
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 87, riskLevel: "low", categories: mockCategories, evidence: [], gaps: [] }));
|
|
66
|
+
const output = lastFrame();
|
|
67
|
+
expect(output).toContain('Security');
|
|
68
|
+
expect(output).toContain('92');
|
|
69
|
+
expect(output).toContain('Availability');
|
|
70
|
+
expect(output).toContain('88');
|
|
71
|
+
expect(output).toContain('Privacy');
|
|
72
|
+
expect(output).toContain('85');
|
|
73
|
+
});
|
|
74
|
+
it('shows gap counts per category', () => {
|
|
75
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 87, riskLevel: "low", categories: mockCategories, evidence: [], gaps: [] }));
|
|
76
|
+
const output = lastFrame();
|
|
77
|
+
// Categories should show their gap counts
|
|
78
|
+
expect(output).toMatch(/Security.*2|2.*Security/);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('renders evidence collection list', () => {
|
|
82
|
+
it('renders evidence collection list', () => {
|
|
83
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 87, riskLevel: "low", categories: [], evidence: mockEvidence, gaps: [] }));
|
|
84
|
+
const output = lastFrame();
|
|
85
|
+
expect(output).toContain('Evidence');
|
|
86
|
+
expect(output).toContain('Access Control Policy');
|
|
87
|
+
expect(output).toContain('Encryption Audit');
|
|
88
|
+
});
|
|
89
|
+
it('shows evidence status', () => {
|
|
90
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 87, riskLevel: "low", categories: [], evidence: mockEvidence, gaps: [] }));
|
|
91
|
+
const output = lastFrame();
|
|
92
|
+
// Should indicate valid vs expired
|
|
93
|
+
expect(output).toMatch(/valid|expired/i);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('renders gap count with severity', () => {
|
|
97
|
+
it('renders gap count with severity', () => {
|
|
98
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 87, riskLevel: "low", categories: [], evidence: [], gaps: mockGaps }));
|
|
99
|
+
const output = lastFrame();
|
|
100
|
+
expect(output).toContain('Gap');
|
|
101
|
+
expect(output).toContain('Missing encryption at rest');
|
|
102
|
+
});
|
|
103
|
+
it('shows severity indicators', () => {
|
|
104
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 87, riskLevel: "low", categories: [], evidence: [], gaps: mockGaps }));
|
|
105
|
+
const output = lastFrame();
|
|
106
|
+
expect(output).toMatch(/high|medium|low/i);
|
|
107
|
+
});
|
|
108
|
+
it('shows gap status', () => {
|
|
109
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 87, riskLevel: "low", categories: [], evidence: [], gaps: mockGaps }));
|
|
110
|
+
const output = lastFrame();
|
|
111
|
+
expect(output).toMatch(/open|in-progress/i);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('download report button works', () => {
|
|
115
|
+
it('download report button shows when handler provided', () => {
|
|
116
|
+
const onDownloadReport = vi.fn();
|
|
117
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 87, riskLevel: "low", categories: [], evidence: [], gaps: [], onDownloadReport: onDownloadReport, isActive: true }));
|
|
118
|
+
const output = lastFrame();
|
|
119
|
+
expect(output).toMatch(/\[d\]|download|report/i);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('shows audit timeline', () => {
|
|
123
|
+
it('shows audit timeline', () => {
|
|
124
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 87, riskLevel: "low", categories: [], evidence: [], gaps: [], auditTimeline: mockTimeline }));
|
|
125
|
+
const output = lastFrame();
|
|
126
|
+
expect(output).toContain('Timeline');
|
|
127
|
+
expect(output).toContain('SOC 2 Type II Audit');
|
|
128
|
+
expect(output).toContain('Internal Review');
|
|
129
|
+
});
|
|
130
|
+
it('shows timeline event status', () => {
|
|
131
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 87, riskLevel: "low", categories: [], evidence: [], gaps: [], auditTimeline: mockTimeline }));
|
|
132
|
+
const output = lastFrame();
|
|
133
|
+
expect(output).toMatch(/upcoming|completed/i);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('handles loading state', () => {
|
|
137
|
+
it('handles loading state', () => {
|
|
138
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 0, riskLevel: "low", categories: [], evidence: [], gaps: [], loading: true }));
|
|
139
|
+
const output = lastFrame();
|
|
140
|
+
expect(output).toMatch(/loading|fetching/i);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('handles error state', () => {
|
|
144
|
+
it('handles error state', () => {
|
|
145
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 0, riskLevel: "low", categories: [], evidence: [], gaps: [], error: "Failed to fetch compliance data" }));
|
|
146
|
+
const output = lastFrame();
|
|
147
|
+
expect(output).toMatch(/error|failed/i);
|
|
148
|
+
expect(output).toContain('Failed to fetch compliance data');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe('refresh button reloads data', () => {
|
|
152
|
+
it('refresh button shows when handler provided', () => {
|
|
153
|
+
const onRefresh = vi.fn();
|
|
154
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 87, riskLevel: "low", categories: [], evidence: [], gaps: [], onRefresh: onRefresh, isActive: true }));
|
|
155
|
+
const output = lastFrame();
|
|
156
|
+
expect(output).toMatch(/\[r\]|refresh|reload/i);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe('shows last report date', () => {
|
|
160
|
+
it('shows last report date', () => {
|
|
161
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 87, riskLevel: "low", categories: [], evidence: [], gaps: [], lastReportDate: "2024-01-15" }));
|
|
162
|
+
const output = lastFrame();
|
|
163
|
+
expect(output).toContain('2024-01-15');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
describe('risk level indicator', () => {
|
|
167
|
+
it('shows risk level low', () => {
|
|
168
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 90, riskLevel: "low", categories: [], evidence: [], gaps: [] }));
|
|
169
|
+
const output = lastFrame();
|
|
170
|
+
expect(output).toMatch(/low/i);
|
|
171
|
+
});
|
|
172
|
+
it('shows risk level high', () => {
|
|
173
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 45, riskLevel: "high", categories: [], evidence: [], gaps: [] }));
|
|
174
|
+
const output = lastFrame();
|
|
175
|
+
expect(output).toMatch(/high/i);
|
|
176
|
+
});
|
|
177
|
+
it('shows risk level critical', () => {
|
|
178
|
+
const { lastFrame } = render(_jsx(CompliancePane, { score: 25, riskLevel: "critical", categories: [], evidence: [], gaps: [] }));
|
|
179
|
+
const output = lastFrame();
|
|
180
|
+
expect(output).toMatch(/critical/i);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface Provider {
|
|
2
|
+
name: string;
|
|
3
|
+
type: 'oauth' | 'saml';
|
|
4
|
+
status: 'connected' | 'error';
|
|
5
|
+
lastSync?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RoleMapping {
|
|
9
|
+
providerGroup: string;
|
|
10
|
+
localRole: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SessionSummary {
|
|
13
|
+
active: number;
|
|
14
|
+
total: number;
|
|
15
|
+
byProvider: Record<string, number>;
|
|
16
|
+
}
|
|
17
|
+
export interface MfaStats {
|
|
18
|
+
enrolled: number;
|
|
19
|
+
total: number;
|
|
20
|
+
pending: number;
|
|
21
|
+
methods: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface SSOPaneProps {
|
|
24
|
+
providers: Provider[];
|
|
25
|
+
roleMappings: RoleMapping[];
|
|
26
|
+
sessions: SessionSummary;
|
|
27
|
+
mfaStats: MfaStats;
|
|
28
|
+
onAddProvider?: () => void;
|
|
29
|
+
onRemoveProvider?: (name: string) => void;
|
|
30
|
+
onRefresh?: () => void;
|
|
31
|
+
loading?: boolean;
|
|
32
|
+
error?: string | null;
|
|
33
|
+
isActive?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function SSOPane({ providers, roleMappings, sessions, mfaStats, onAddProvider, onRemoveProvider, onRefresh, loading, error, isActive, }: SSOPaneProps): import("react/jsx-runtime").JSX.Element;
|
|
36
|
+
export default SSOPane;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
function formatTimestamp(ts) {
|
|
4
|
+
if (!ts)
|
|
5
|
+
return '';
|
|
6
|
+
try {
|
|
7
|
+
const date = new Date(ts);
|
|
8
|
+
if (isNaN(date.getTime()))
|
|
9
|
+
return '';
|
|
10
|
+
const hours = String(date.getUTCHours()).padStart(2, '0');
|
|
11
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
|
12
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
|
13
|
+
return `${hours}:${minutes}:${seconds}`;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function ProvidersList({ providers }) {
|
|
20
|
+
if (providers.length === 0) {
|
|
21
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "Identity Providers:" }), _jsx(Text, { color: "gray", children: " No providers configured" })] }));
|
|
22
|
+
}
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "Identity Providers:" }), providers.map((provider) => {
|
|
24
|
+
const statusColor = provider.status === 'connected' ? 'green' : 'red';
|
|
25
|
+
const statusLabel = provider.status === 'connected' ? 'connected' : 'error';
|
|
26
|
+
const timestamp = formatTimestamp(provider.lastSync);
|
|
27
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: provider.name }), _jsxs(Text, { dimColor: true, children: [" (", provider.type, ") "] }), _jsxs(Text, { color: statusColor, children: ["[", statusLabel, "]"] }), timestamp && _jsxs(Text, { dimColor: true, children: [" Last sync: ", timestamp] })] }), provider.error && (_jsx(Box, { marginLeft: 4, children: _jsx(Text, { color: "red", children: provider.error }) }))] }, provider.name));
|
|
28
|
+
})] }));
|
|
29
|
+
}
|
|
30
|
+
function RoleMappingTable({ mappings }) {
|
|
31
|
+
if (mappings.length === 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "Role Mapping:" }), mappings.map((mapping, idx) => (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { children: mapping.providerGroup }), _jsx(Text, { dimColor: true, children: " -> " }), _jsx(Text, { color: "cyan", children: mapping.localRole })] }, idx)))] }));
|
|
35
|
+
}
|
|
36
|
+
function SessionsDisplay({ sessions }) {
|
|
37
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "Sessions:" }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " Active: " }), _jsx(Text, { color: "green", children: sessions.active }), _jsx(Text, { dimColor: true, children: " / Total: " }), _jsx(Text, { children: sessions.total })] }), Object.keys(sessions.byProvider).length > 0 && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " By provider: " }), _jsx(Text, { children: Object.entries(sessions.byProvider)
|
|
38
|
+
.map(([name, count]) => `${name}: ${count}`)
|
|
39
|
+
.join(', ') })] }))] }));
|
|
40
|
+
}
|
|
41
|
+
function MfaDisplay({ stats }) {
|
|
42
|
+
const percentage = stats.total > 0 ? Math.round((stats.enrolled / stats.total) * 100) : 0;
|
|
43
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "MFA Enrollment:" }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " Enrolled: " }), _jsx(Text, { color: percentage >= 80 ? 'green' : percentage >= 50 ? 'yellow' : 'red', children: stats.enrolled }), _jsx(Text, { dimColor: true, children: " / " }), _jsx(Text, { children: stats.total }), _jsxs(Text, { dimColor: true, children: [" (", percentage, "%)"] })] }), stats.pending > 0 && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " Pending: " }), _jsx(Text, { color: "yellow", children: stats.pending })] })), stats.methods.length > 0 && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " Methods: " }), _jsx(Text, { children: stats.methods.join(', ') })] }))] }));
|
|
44
|
+
}
|
|
45
|
+
function LoadingIndicator() {
|
|
46
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "cyan", children: "Loading SSO configuration..." }) }));
|
|
47
|
+
}
|
|
48
|
+
function ErrorDisplay({ error }) {
|
|
49
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsx(Text, { color: "red", bold: true, children: "Error" }), _jsx(Text, { color: "red", children: error })] }));
|
|
50
|
+
}
|
|
51
|
+
export function SSOPane({ providers, roleMappings, sessions, mfaStats, onAddProvider, onRemoveProvider, onRefresh, loading = false, error = null, isActive = false, }) {
|
|
52
|
+
useInput((input, _key) => {
|
|
53
|
+
if (!isActive)
|
|
54
|
+
return;
|
|
55
|
+
// Add provider with 'a'
|
|
56
|
+
if (input === 'a' && onAddProvider) {
|
|
57
|
+
onAddProvider();
|
|
58
|
+
}
|
|
59
|
+
// Delete/remove provider with 'd'
|
|
60
|
+
if (input === 'd' && onRemoveProvider && providers.length > 0) {
|
|
61
|
+
// Remove first provider for simplicity; in real app would have selection
|
|
62
|
+
onRemoveProvider(providers[0].name);
|
|
63
|
+
}
|
|
64
|
+
// Refresh with 'r'
|
|
65
|
+
if (input === 'r' && onRefresh) {
|
|
66
|
+
onRefresh();
|
|
67
|
+
}
|
|
68
|
+
}, { isActive });
|
|
69
|
+
return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "SSO Management" }), providers.length > 0 && (_jsxs(Text, { dimColor: true, children: [" (", providers.length, " provider", providers.length !== 1 ? 's' : '', ")"] }))] }), loading && _jsx(LoadingIndicator, {}), error && _jsx(ErrorDisplay, { error: error }), !loading && (_jsxs(_Fragment, { children: [_jsx(ProvidersList, { providers: providers }), _jsx(RoleMappingTable, { mappings: roleMappings }), _jsx(SessionsDisplay, { sessions: sessions }), _jsx(MfaDisplay, { stats: mfaStats })] })), isActive && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [onAddProvider && '[a] Add provider ', onRemoveProvider && providers.length > 0 && '[d] Remove provider ', onRefresh && '[r] Refresh'] }) }))] }));
|
|
70
|
+
}
|
|
71
|
+
export default SSOPane;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { SSOPane } from './SSOPane.js';
|
|
5
|
+
describe('SSOPane', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
const mockProviders = [
|
|
13
|
+
{
|
|
14
|
+
name: 'GitHub',
|
|
15
|
+
type: 'oauth',
|
|
16
|
+
status: 'connected',
|
|
17
|
+
lastSync: '2024-01-15T10:30:00.000Z',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'Okta',
|
|
21
|
+
type: 'saml',
|
|
22
|
+
status: 'connected',
|
|
23
|
+
lastSync: '2024-01-15T09:00:00.000Z',
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
const mockRoleMappings = [
|
|
27
|
+
{ providerGroup: 'admins', localRole: 'admin' },
|
|
28
|
+
{ providerGroup: 'developers', localRole: 'developer' },
|
|
29
|
+
{ providerGroup: 'viewers', localRole: 'viewer' },
|
|
30
|
+
];
|
|
31
|
+
const mockSessions = {
|
|
32
|
+
active: 15,
|
|
33
|
+
total: 42,
|
|
34
|
+
byProvider: {
|
|
35
|
+
GitHub: 10,
|
|
36
|
+
Okta: 5,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const mockMfaStats = {
|
|
40
|
+
enrolled: 38,
|
|
41
|
+
total: 42,
|
|
42
|
+
pending: 4,
|
|
43
|
+
methods: ['totp', 'webauthn'],
|
|
44
|
+
};
|
|
45
|
+
describe('renders provider list correctly', () => {
|
|
46
|
+
it('renders provider list correctly', () => {
|
|
47
|
+
const { lastFrame } = render(_jsx(SSOPane, { providers: mockProviders, roleMappings: [], sessions: { active: 0, total: 0, byProvider: {} }, mfaStats: { enrolled: 0, total: 0, pending: 0, methods: [] } }));
|
|
48
|
+
const output = lastFrame();
|
|
49
|
+
expect(output).toContain('SSO');
|
|
50
|
+
expect(output).toContain('GitHub');
|
|
51
|
+
expect(output).toContain('Okta');
|
|
52
|
+
expect(output).toMatch(/oauth/i);
|
|
53
|
+
expect(output).toMatch(/saml/i);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('renders empty state when no providers', () => {
|
|
57
|
+
it('renders empty state when no providers', () => {
|
|
58
|
+
const { lastFrame } = render(_jsx(SSOPane, { providers: [], roleMappings: [], sessions: { active: 0, total: 0, byProvider: {} }, mfaStats: { enrolled: 0, total: 0, pending: 0, methods: [] } }));
|
|
59
|
+
const output = lastFrame();
|
|
60
|
+
expect(output).toContain('SSO');
|
|
61
|
+
expect(output).toMatch(/no.*provider|empty|none configured/i);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('shows connected status for working providers', () => {
|
|
65
|
+
it('shows connected status for working providers', () => {
|
|
66
|
+
const { lastFrame } = render(_jsx(SSOPane, { providers: mockProviders, roleMappings: [], sessions: { active: 0, total: 0, byProvider: {} }, mfaStats: { enrolled: 0, total: 0, pending: 0, methods: [] } }));
|
|
67
|
+
const output = lastFrame();
|
|
68
|
+
expect(output).toMatch(/connected|active|ok/i);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('shows error status for failing providers', () => {
|
|
72
|
+
it('shows error status for failing providers', () => {
|
|
73
|
+
const errorProviders = [
|
|
74
|
+
{
|
|
75
|
+
name: 'BrokenIdP',
|
|
76
|
+
type: 'saml',
|
|
77
|
+
status: 'error',
|
|
78
|
+
error: 'Certificate expired',
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
const { lastFrame } = render(_jsx(SSOPane, { providers: errorProviders, roleMappings: [], sessions: { active: 0, total: 0, byProvider: {} }, mfaStats: { enrolled: 0, total: 0, pending: 0, methods: [] } }));
|
|
82
|
+
const output = lastFrame();
|
|
83
|
+
expect(output).toContain('BrokenIdP');
|
|
84
|
+
expect(output).toMatch(/error|failed|certificate/i);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('add provider button opens modal', () => {
|
|
88
|
+
it('add provider button shows when handler provided', () => {
|
|
89
|
+
const onAddProvider = vi.fn();
|
|
90
|
+
const { lastFrame } = render(_jsx(SSOPane, { providers: [], roleMappings: [], sessions: { active: 0, total: 0, byProvider: {} }, mfaStats: { enrolled: 0, total: 0, pending: 0, methods: [] }, onAddProvider: onAddProvider, isActive: true }));
|
|
91
|
+
const output = lastFrame();
|
|
92
|
+
expect(output).toMatch(/\[a\]|add.*provider/i);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('remove provider shows confirmation', () => {
|
|
96
|
+
it('remove provider control shows when handler provided', () => {
|
|
97
|
+
const onRemoveProvider = vi.fn();
|
|
98
|
+
const { lastFrame } = render(_jsx(SSOPane, { providers: mockProviders, roleMappings: [], sessions: { active: 0, total: 0, byProvider: {} }, mfaStats: { enrolled: 0, total: 0, pending: 0, methods: [] }, onRemoveProvider: onRemoveProvider, isActive: true }));
|
|
99
|
+
const output = lastFrame();
|
|
100
|
+
expect(output).toMatch(/\[d\]|remove|delete/i);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('shows role mapping table', () => {
|
|
104
|
+
it('shows role mapping table', () => {
|
|
105
|
+
const { lastFrame } = render(_jsx(SSOPane, { providers: mockProviders, roleMappings: mockRoleMappings, sessions: { active: 0, total: 0, byProvider: {} }, mfaStats: { enrolled: 0, total: 0, pending: 0, methods: [] } }));
|
|
106
|
+
const output = lastFrame();
|
|
107
|
+
expect(output).toMatch(/role.*mapping|mapping/i);
|
|
108
|
+
expect(output).toContain('admins');
|
|
109
|
+
expect(output).toContain('admin');
|
|
110
|
+
expect(output).toContain('developers');
|
|
111
|
+
expect(output).toContain('developer');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('shows active sessions count', () => {
|
|
115
|
+
it('shows active sessions count', () => {
|
|
116
|
+
const { lastFrame } = render(_jsx(SSOPane, { providers: mockProviders, roleMappings: [], sessions: mockSessions, mfaStats: { enrolled: 0, total: 0, pending: 0, methods: [] } }));
|
|
117
|
+
const output = lastFrame();
|
|
118
|
+
expect(output).toMatch(/session/i);
|
|
119
|
+
expect(output).toContain('15');
|
|
120
|
+
expect(output).toContain('42');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('shows MFA enrollment stats', () => {
|
|
124
|
+
it('shows MFA enrollment stats', () => {
|
|
125
|
+
const { lastFrame } = render(_jsx(SSOPane, { providers: mockProviders, roleMappings: [], sessions: { active: 0, total: 0, byProvider: {} }, mfaStats: mockMfaStats }));
|
|
126
|
+
const output = lastFrame();
|
|
127
|
+
expect(output).toMatch(/mfa|multi-factor/i);
|
|
128
|
+
expect(output).toContain('38');
|
|
129
|
+
expect(output).toContain('42');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('handles loading state', () => {
|
|
133
|
+
it('handles loading state', () => {
|
|
134
|
+
const { lastFrame } = render(_jsx(SSOPane, { providers: [], roleMappings: [], sessions: { active: 0, total: 0, byProvider: {} }, mfaStats: { enrolled: 0, total: 0, pending: 0, methods: [] }, loading: true }));
|
|
135
|
+
const output = lastFrame();
|
|
136
|
+
expect(output).toMatch(/loading|fetching/i);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('handles error state', () => {
|
|
140
|
+
it('handles error state', () => {
|
|
141
|
+
const { lastFrame } = render(_jsx(SSOPane, { providers: [], roleMappings: [], sessions: { active: 0, total: 0, byProvider: {} }, mfaStats: { enrolled: 0, total: 0, pending: 0, methods: [] }, error: "Failed to fetch SSO configuration" }));
|
|
142
|
+
const output = lastFrame();
|
|
143
|
+
expect(output).toMatch(/error|failed/i);
|
|
144
|
+
expect(output).toContain('Failed to fetch SSO configuration');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('refresh button reloads data', () => {
|
|
148
|
+
it('refresh button shows when handler provided', () => {
|
|
149
|
+
const onRefresh = vi.fn();
|
|
150
|
+
const { lastFrame } = render(_jsx(SSOPane, { providers: mockProviders, roleMappings: [], sessions: { active: 0, total: 0, byProvider: {} }, mfaStats: { enrolled: 0, total: 0, pending: 0, methods: [] }, onRefresh: onRefresh, isActive: true }));
|
|
151
|
+
const output = lastFrame();
|
|
152
|
+
expect(output).toMatch(/\[r\]|refresh|reload/i);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
/**
|
|
5
|
-
* Parse markdown content and render as terminal-friendly text
|
|
6
|
-
*/
|
|
7
|
-
function renderMarkdownLine(line) {
|
|
8
|
-
// Heading detection
|
|
9
|
-
if (line.startsWith('# ')) {
|
|
10
|
-
return { text: line.slice(2), bold: true, dimColor: false, color: 'cyan' };
|
|
11
|
-
}
|
|
12
|
-
if (line.startsWith('## ')) {
|
|
13
|
-
return { text: line.slice(3), bold: true, dimColor: false, color: 'cyan' };
|
|
14
|
-
}
|
|
15
|
-
if (line.startsWith('### ')) {
|
|
16
|
-
return { text: line.slice(4), bold: true, dimColor: false };
|
|
17
|
-
}
|
|
18
|
-
return { text: line, bold: false, dimColor: false };
|
|
19
|
-
}
|
|
20
4
|
function parseMarkdown(content) {
|
|
21
5
|
const blocks = [];
|
|
22
6
|
const lines = content.split('\n');
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface RetentionRule {
|
|
2
|
+
retention: string;
|
|
3
|
+
persist: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface RetentionPolicy {
|
|
6
|
+
retention?: string;
|
|
7
|
+
persist?: boolean;
|
|
8
|
+
sensitivityLevels?: Record<string, RetentionRule>;
|
|
9
|
+
dataTypes?: Record<string, RetentionRule>;
|
|
10
|
+
}
|
|
11
|
+
export interface PurgeEntry {
|
|
12
|
+
id: string;
|
|
13
|
+
timestamp: string;
|
|
14
|
+
itemCount: number;
|
|
15
|
+
dataTypes: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface SensitiveDataInfo {
|
|
18
|
+
detected: boolean;
|
|
19
|
+
count: number;
|
|
20
|
+
types: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface SubsystemStatus {
|
|
23
|
+
ephemeralStorage?: boolean;
|
|
24
|
+
sessionPurge?: boolean;
|
|
25
|
+
memoryExclusion?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface ValidationResult {
|
|
28
|
+
valid: boolean;
|
|
29
|
+
conflicts: string[];
|
|
30
|
+
warnings: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface ZeroRetentionPaneProps {
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
policy?: RetentionPolicy;
|
|
35
|
+
purgeHistory?: PurgeEntry[];
|
|
36
|
+
sensitiveDataDetected?: SensitiveDataInfo;
|
|
37
|
+
subsystems?: SubsystemStatus;
|
|
38
|
+
validation?: ValidationResult;
|
|
39
|
+
isActive?: boolean;
|
|
40
|
+
onToggle?: () => void;
|
|
41
|
+
onPurge?: () => void;
|
|
42
|
+
}
|
|
43
|
+
export declare function ZeroRetentionPane({ enabled, policy, purgeHistory, sensitiveDataDetected, subsystems, validation, isActive, onToggle, onPurge, }: ZeroRetentionPaneProps): import("react/jsx-runtime").JSX.Element;
|
|
44
|
+
export default ZeroRetentionPane;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
function formatTimestamp(ts) {
|
|
4
|
+
if (!ts)
|
|
5
|
+
return '';
|
|
6
|
+
try {
|
|
7
|
+
const date = new Date(ts);
|
|
8
|
+
if (isNaN(date.getTime()))
|
|
9
|
+
return '';
|
|
10
|
+
const hours = String(date.getUTCHours()).padStart(2, '0');
|
|
11
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
|
12
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
|
13
|
+
return `${hours}:${minutes}:${seconds}`;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function StatusIndicator({ enabled }) {
|
|
20
|
+
return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Status: " }), _jsx(Text, { color: enabled ? 'green' : 'gray', bold: true, children: enabled ? 'ENABLED' : 'DISABLED' })] }));
|
|
21
|
+
}
|
|
22
|
+
function SubsystemsList({ subsystems }) {
|
|
23
|
+
const items = [
|
|
24
|
+
{ key: 'ephemeralStorage', label: 'Ephemeral Storage' },
|
|
25
|
+
{ key: 'sessionPurge', label: 'Session Purge' },
|
|
26
|
+
{ key: 'memoryExclusion', label: 'Memory Exclusion' },
|
|
27
|
+
];
|
|
28
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "Subsystems:" }), items.map((item) => {
|
|
29
|
+
const active = subsystems[item.key];
|
|
30
|
+
return (_jsx(Box, { children: _jsxs(Text, { color: active ? 'green' : 'gray', children: [active ? ' [x]' : ' [ ]', " ", item.label] }) }, item.key));
|
|
31
|
+
})] }));
|
|
32
|
+
}
|
|
33
|
+
function PolicySummary({ policy }) {
|
|
34
|
+
const rules = [];
|
|
35
|
+
if (policy.retention) {
|
|
36
|
+
rules.push({ label: 'Default', value: policy.retention });
|
|
37
|
+
}
|
|
38
|
+
if (policy.sensitivityLevels) {
|
|
39
|
+
Object.entries(policy.sensitivityLevels).forEach(([level, rule]) => {
|
|
40
|
+
rules.push({ label: level, value: rule.retention });
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (policy.dataTypes) {
|
|
44
|
+
Object.entries(policy.dataTypes).forEach(([type, rule]) => {
|
|
45
|
+
rules.push({ label: type, value: rule.retention });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "Policy:" }), rules.length === 0 ? (_jsx(Text, { color: "gray", children: " No policy rules configured" })) : (rules.slice(0, 5).map((rule, idx) => (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [" ", rule.label, ": "] }), _jsx(Text, { color: rule.value === 'immediate' ? 'cyan' : 'white', children: rule.value })] }, idx)))), rules.length > 5 && (_jsxs(Text, { dimColor: true, children: [" ... and ", rules.length - 5, " more rules"] }))] }));
|
|
49
|
+
}
|
|
50
|
+
function PurgeHistory({ entries }) {
|
|
51
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "Recent Purge Activity:" }), entries.length === 0 ? (_jsx(Text, { color: "gray", children: " No purge history - none yet" })) : (entries.slice(0, 5).map((entry) => {
|
|
52
|
+
const timestamp = formatTimestamp(entry.timestamp);
|
|
53
|
+
return (_jsxs(Box, { children: [timestamp && _jsxs(Text, { dimColor: true, children: [" [", timestamp, "] "] }), _jsxs(Text, { children: ["Purged ", entry.itemCount, " items "] }), _jsxs(Text, { dimColor: true, children: ["(", entry.dataTypes.join(', '), ")"] })] }, entry.id));
|
|
54
|
+
}))] }));
|
|
55
|
+
}
|
|
56
|
+
function SensitiveWarning({ info }) {
|
|
57
|
+
if (!info.detected || info.count === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsx(Box, { children: _jsx(Text, { color: "yellow", bold: true, children: "Warning: Sensitive Data Detected" }) }), _jsx(Box, { children: _jsxs(Text, { color: "yellow", children: ["Found ", info.count, " sensitive item(s): ", info.types.join(', ')] }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Enable zero-retention mode to protect this data" }) })] }));
|
|
61
|
+
}
|
|
62
|
+
function ValidationStatus({ validation }) {
|
|
63
|
+
if (validation.valid && validation.warnings.length === 0) {
|
|
64
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", children: "Configuration: Valid and OK" }) }));
|
|
65
|
+
}
|
|
66
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [validation.conflicts.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", bold: true, children: "Conflicts:" }), validation.conflicts.map((conflict, idx) => (_jsxs(Text, { color: "red", children: [" - ", conflict] }, idx)))] })), validation.warnings.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", bold: true, children: "Warnings:" }), validation.warnings.map((warning, idx) => (_jsxs(Text, { color: "yellow", children: [" - ", warning] }, idx)))] }))] }));
|
|
67
|
+
}
|
|
68
|
+
export function ZeroRetentionPane({ enabled = false, policy, purgeHistory = [], sensitiveDataDetected, subsystems, validation, isActive = false, onToggle, onPurge, }) {
|
|
69
|
+
useInput((input, _key) => {
|
|
70
|
+
if (!isActive)
|
|
71
|
+
return;
|
|
72
|
+
// Toggle with 't'
|
|
73
|
+
if (input === 't' && onToggle) {
|
|
74
|
+
onToggle();
|
|
75
|
+
}
|
|
76
|
+
// Force purge with 'p'
|
|
77
|
+
if (input === 'p' && onPurge) {
|
|
78
|
+
onPurge();
|
|
79
|
+
}
|
|
80
|
+
}, { isActive });
|
|
81
|
+
return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Zero-Retention Mode" }) }), _jsx(StatusIndicator, { enabled: enabled }), sensitiveDataDetected && (_jsx(SensitiveWarning, { info: sensitiveDataDetected })), validation && _jsx(ValidationStatus, { validation: validation }), subsystems && _jsx(SubsystemsList, { subsystems: subsystems }), policy && _jsx(PolicySummary, { policy: policy }), _jsx(PurgeHistory, { entries: purgeHistory }), isActive && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["[t] Toggle ", enabled ? 'disable' : 'enable', enabled && ' [p] Force purge'] }) }))] }));
|
|
82
|
+
}
|
|
83
|
+
export default ZeroRetentionPane;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|