aidevops 3.32.5 → 3.32.6
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/README.md +10 -10
- package/VERSION +1 -1
- package/aidevops.sh +1 -1
- package/package.json +1 -1
- package/packages/gui-api/src/status-adapter.ts +1 -0
- package/packages/gui-api/src/status-vault.ts +61 -18
- package/packages/gui-desktop/scripts/install-macos-app.sh +88 -3
- package/packages/gui-shared/src/fixtures.ts +1 -6
- package/packages/gui-web/src/App.tsx +10 -56
- package/packages/gui-web/src/AppWorkspace.tsx +5 -5
- package/packages/gui-web/src/DesktopStatusBar.tsx +1 -1
- package/packages/gui-web/src/SecuritySurface.tsx +187 -0
- package/packages/gui-web/src/StatusSurfaces.tsx +4 -170
- package/packages/gui-web/src/VaultAccessModal.tsx +123 -0
- package/packages/gui-web/src/VaultBadges.tsx +96 -36
- package/packages/gui-web/src/VaultSurface.tsx +282 -0
- package/packages/gui-web/src/status-client.ts +72 -24
- package/packages/gui-web/src/styles.css +385 -34
- package/packages/gui-web/src/useAppNavigation.ts +55 -0
- package/packages/gui-web/src/useGuiStatus.ts +79 -0
- package/packages/gui-web/src/useVaultAccessDialog.ts +160 -0
- package/packages/gui-web/src/vault-command-bridge.ts +27 -0
- package/setup.sh +3 -1
- package/packages/gui-web/src/VaultPassphraseModal.tsx +0 -147
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { GuiSecretReference, GuiStatusData, GuiVaultStatusData } from "@aidevops/gui-shared";
|
|
2
|
+
import { type ReactElement, useMemo, useState } from "react";
|
|
3
|
+
import { FiActivity, FiAlertTriangle, FiCheckCircle, FiKey, FiLink, FiLock, FiSearch, FiShield, FiTerminal } from "react-icons/fi";
|
|
4
|
+
import { type VaultDialogIntent, vaultActionLabel, vaultDialogIntentForStatus } from "./VaultBadges";
|
|
5
|
+
|
|
6
|
+
type SecretFilter = "all" | "configured" | "attention";
|
|
7
|
+
|
|
8
|
+
export function SecuritySurface({ onVaultRequest, status }: {
|
|
9
|
+
onVaultRequest: (intent: VaultDialogIntent) => void;
|
|
10
|
+
status: GuiStatusData;
|
|
11
|
+
}): ReactElement {
|
|
12
|
+
const [filter, setFilter] = useState<SecretFilter>("all");
|
|
13
|
+
const [query, setQuery] = useState("");
|
|
14
|
+
const vault = status.vault;
|
|
15
|
+
const intent = vaultDialogIntentForStatus(vault);
|
|
16
|
+
const configured = status.secrets.filter((secret) => secret.status === "configured").length;
|
|
17
|
+
const needsAttention = status.secrets.length - configured;
|
|
18
|
+
const hiddenCount = vault.unlocked ? undefined : "Hidden";
|
|
19
|
+
const filteredSecrets = useMemo(
|
|
20
|
+
() => status.secrets.filter((secret) => secretMatches(secret, filter, query)),
|
|
21
|
+
[filter, query, status.secrets],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<section className="surface-page secrets-surface" aria-label="Secrets">
|
|
26
|
+
<div className="hero-panel secrets-hero">
|
|
27
|
+
<div className="section-heading split-heading">
|
|
28
|
+
<div>
|
|
29
|
+
<p className="eyebrow">Secret references · metadata only</p>
|
|
30
|
+
<h2>Secrets</h2>
|
|
31
|
+
<p>Understand credential readiness and dependencies without exposing, copying, or sending secret values anywhere.</p>
|
|
32
|
+
</div>
|
|
33
|
+
<div className="secrets-hero-actions">
|
|
34
|
+
<span className={`secrets-state-badge ${vaultStateTone(vault)}`}><VaultStateIcon vault={vault} /> {vaultStateLabel(vault)}</span>
|
|
35
|
+
<button className={intent === "setup" || intent === "unlock" ? "primary-action" : "secondary-action"} onClick={() => onVaultRequest(intent)} type="button">{vaultActionLabel(intent)}</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="secrets-boundary" role="note">
|
|
39
|
+
<FiShield aria-hidden="true" />
|
|
40
|
+
<div><strong>Values stay out of the interface.</strong><span>Only reference names and non-sensitive health metadata become visible after local Vault unlock. Values are never returned, rendered, copied, exported, logged, or sent to AI.</span></div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{/* biome-ignore lint/a11y/useSemanticElements: role=group preserves the existing grid element and requested accessible grouping. */}
|
|
45
|
+
<div className="secrets-metric-grid" aria-label="Secrets summary" role="group">
|
|
46
|
+
<SecretMetric detail={vault.unlocked ? "metadata records" : "visible after local unlock"} icon={<FiKey />} label="References" value={hiddenCount ?? String(status.secrets.length)} />
|
|
47
|
+
<SecretMetric detail={vault.unlocked ? "ready for dependent tools" : "visible after local unlock"} icon={<FiCheckCircle />} label="Configured" value={hiddenCount ?? String(configured)} />
|
|
48
|
+
<SecretMetric detail={vault.unlocked ? "missing or not yet checked" : "visible after local unlock"} icon={<FiAlertTriangle />} label="Needs attention" value={hiddenCount ?? String(needsAttention)} />
|
|
49
|
+
<SecretMetric detail="hidden local prompt only" icon={<FiTerminal />} label="Value custody" value="Write-only" />
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{vault.unlocked ? (
|
|
53
|
+
<UnlockedSecrets filter={filter} filteredSecrets={filteredSecrets} onFilterChange={setFilter} onQueryChange={setQuery} query={query} total={status.secrets.length} />
|
|
54
|
+
) : (
|
|
55
|
+
<LockedSecrets intent={intent} onVaultRequest={onVaultRequest} vault={vault} />
|
|
56
|
+
)}
|
|
57
|
+
</section>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function LockedSecrets({ intent, onVaultRequest, vault }: { intent: VaultDialogIntent; onVaultRequest: (intent: VaultDialogIntent) => void; vault: GuiVaultStatusData }): ReactElement {
|
|
62
|
+
const guidance = vaultGuidance(vault);
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
<section className={`panel secrets-guidance ${vault.status === "corrupted" ? "is-error" : ""}`} aria-label="Vault access guidance">
|
|
66
|
+
<div className="section-heading split-heading">
|
|
67
|
+
<div>
|
|
68
|
+
<p className="eyebrow">{guidance.eyebrow}</p>
|
|
69
|
+
<h2>{guidance.title}</h2>
|
|
70
|
+
<p>{guidance.detail}</p>
|
|
71
|
+
</div>
|
|
72
|
+
<button className="secondary-action" onClick={() => onVaultRequest(intent)} type="button">{vaultActionLabel(intent)}</button>
|
|
73
|
+
</div>
|
|
74
|
+
<div className="secrets-evidence-strip" role="status">
|
|
75
|
+
<span><strong>Vault</strong>{vault.status}</span>
|
|
76
|
+
<span><strong>Setup</strong>{vault.setup_state}</span>
|
|
77
|
+
<span><strong>Helper</strong>{vault.helper_status}</span>
|
|
78
|
+
<span><strong>Preview</strong>hidden</span>
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
<section className="panel" aria-label="What Vault unlock enables">
|
|
82
|
+
<div className="section-heading">
|
|
83
|
+
<p className="eyebrow">Protected capabilities</p>
|
|
84
|
+
<h2>What unlock enables</h2>
|
|
85
|
+
<p>Unlock grants this local session access to reference metadata. Secret material remains behind secure storage helpers.</p>
|
|
86
|
+
</div>
|
|
87
|
+
<div className="secrets-capability-grid">
|
|
88
|
+
<SecretCapability detail="Names and non-sensitive configured, missing, and unchecked health states." icon={<FiKey />} title="Reference inventory" />
|
|
89
|
+
<SecretCapability detail="See which integrations, routines, and resources depend on each reference as adapters mature." icon={<FiLink />} title="Dependency awareness" />
|
|
90
|
+
<SecretCapability detail="Run allowlisted checks and rotation handoffs without reveal or copy controls." icon={<FiActivity />} title="Validation and rotation" />
|
|
91
|
+
</div>
|
|
92
|
+
</section>
|
|
93
|
+
</>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function UnlockedSecrets({ filter, filteredSecrets, onFilterChange, onQueryChange, query, total }: {
|
|
98
|
+
filter: SecretFilter;
|
|
99
|
+
filteredSecrets: GuiSecretReference[];
|
|
100
|
+
onFilterChange: (filter: SecretFilter) => void;
|
|
101
|
+
onQueryChange: (query: string) => void;
|
|
102
|
+
query: string;
|
|
103
|
+
total: number;
|
|
104
|
+
}): ReactElement {
|
|
105
|
+
return (
|
|
106
|
+
<section className="panel secrets-inventory" aria-label="Secret reference inventory">
|
|
107
|
+
<div className="section-heading split-heading">
|
|
108
|
+
<div><p className="eyebrow">Unlocked for this local session</p><h2>Reference inventory</h2><p>Metadata can be reviewed; values remain unavailable to the browser.</p></div>
|
|
109
|
+
<span className="count-pill">{filteredSecrets.length} of {total}</span>
|
|
110
|
+
</div>
|
|
111
|
+
<div className="secrets-toolbar">
|
|
112
|
+
<label className="secrets-search"><FiSearch aria-hidden="true" /><span className="sr-only">Search references</span><input onChange={(event) => onQueryChange(event.currentTarget.value)} placeholder="Search reference names" type="search" value={query} /></label>
|
|
113
|
+
{/* biome-ignore lint/a11y/useSemanticElements: role=group preserves the existing toolbar layout and requested accessible grouping. */}
|
|
114
|
+
<div className="secrets-filters" aria-label="Filter references" role="group">
|
|
115
|
+
{(["all", "configured", "attention"] as const).map((option) => <button aria-pressed={filter === option} key={option} onClick={() => onFilterChange(option)} type="button">{option === "attention" ? "Needs attention" : titleCase(option)}</button>)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
{filteredSecrets.length === 0 ? <p className="empty-state">No secret references match this view.</p> : (
|
|
119
|
+
<div className="secrets-table-wrap">
|
|
120
|
+
<table className="secrets-reference-table">
|
|
121
|
+
<thead><tr><th scope="col">Reference</th><th scope="col">Health</th><th scope="col">Value access</th><th scope="col">Management</th></tr></thead>
|
|
122
|
+
<tbody>{filteredSecrets.map((secret) => <SecretRow key={secret.name} secret={secret} />)}</tbody>
|
|
123
|
+
</table>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</section>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function SecretRow({ secret }: { secret: GuiSecretReference }): ReactElement {
|
|
131
|
+
return (
|
|
132
|
+
<tr>
|
|
133
|
+
<td data-label="Reference"><strong>{secret.name}</strong></td>
|
|
134
|
+
<td data-label="Health"><span className={`secret-health ${secret.status}`}>{secret.status}</span></td>
|
|
135
|
+
<td data-label="Value access">Never displayed</td>
|
|
136
|
+
<td data-label="Management"><span className="metadata-only-label"><FiTerminal aria-hidden="true" /> Secure helper</span></td>
|
|
137
|
+
</tr>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function SecretMetric({ detail, icon, label, value }: { detail: string; icon: ReactElement; label: string; value: string }): ReactElement {
|
|
142
|
+
return <article className="secret-metric-card"><span className="secret-metric-icon" aria-hidden="true">{icon}</span><div><span>{label}</span><strong>{value}</strong><small>{detail}</small></div></article>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function SecretCapability({ detail, icon, title }: { detail: string; icon: ReactElement; title: string }): ReactElement {
|
|
146
|
+
return <article className="secret-capability-card"><span aria-hidden="true">{icon}</span><div><h3>{title}</h3><p>{detail}</p></div></article>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function VaultStateIcon({ vault }: { vault: GuiVaultStatusData }): ReactElement {
|
|
150
|
+
if (vault.unlocked) return <FiCheckCircle aria-hidden="true" />;
|
|
151
|
+
if (vault.status === "unknown" || vault.status === "corrupted") return <FiAlertTriangle aria-hidden="true" />;
|
|
152
|
+
return <FiLock aria-hidden="true" />;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function vaultStateLabel(vault: GuiVaultStatusData): string {
|
|
156
|
+
if (vault.unlocked) return "Vault unlocked";
|
|
157
|
+
if (vault.status === "locked") return "Vault locked";
|
|
158
|
+
if (vault.status === "uninitialized") return "Vault not configured";
|
|
159
|
+
if (vault.status === "corrupted") return "Recovery required";
|
|
160
|
+
return "Status unavailable";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function vaultStateTone(vault: GuiVaultStatusData): string {
|
|
164
|
+
if (vault.unlocked) return "success";
|
|
165
|
+
if (vault.status === "corrupted") return "error";
|
|
166
|
+
if (vault.status === "locked" || vault.status === "uninitialized") return "warning";
|
|
167
|
+
return "unknown";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function vaultGuidance(vault: GuiVaultStatusData): { detail: string; eyebrow: string; title: string } {
|
|
171
|
+
if (vault.status === "locked") return { eyebrow: "Local Vault", title: "Unlock protected metadata", detail: "Use the existing passphrase in the local hidden terminal prompt. The app will refresh when you return." };
|
|
172
|
+
if (vault.status === "uninitialized") return { eyebrow: "First-use setup", title: "Create the local Vault", detail: "Initialise once in a local hidden prompt, save the passphrase in a trusted password manager, then complete the restart verification." };
|
|
173
|
+
if (vault.status === "corrupted") return { eyebrow: "Recovery required", title: "Vault metadata needs attention", detail: "Do not initialise over existing data. Review the conservative recovery options before making changes." };
|
|
174
|
+
return { eyebrow: "Status unavailable", title: "Vault readiness could not be verified", detail: "No setup or passphrase action will be offered until both local status probes return authoritative metadata." };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function secretMatches(secret: GuiSecretReference, filter: SecretFilter, query: string): boolean {
|
|
178
|
+
const matchesQuery = secret.name.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase());
|
|
179
|
+
if (!matchesQuery) return false;
|
|
180
|
+
if (filter === "configured") return secret.status === "configured";
|
|
181
|
+
if (filter === "attention") return secret.status !== "configured";
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function titleCase(value: string): string {
|
|
186
|
+
return `${value.charAt(0).toLocaleUpperCase()}${value.slice(1)}`;
|
|
187
|
+
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/* jshint esversion: 11 */
|
|
2
2
|
import { useState } from "react";
|
|
3
|
-
import type { GuiAiAppSummary, GuiLocalRepoSetupSummary, GuiSetupTargetSummary, GuiStatusData
|
|
3
|
+
import type { GuiAiAppSummary, GuiLocalRepoSetupSummary, GuiSetupTargetSummary, GuiStatusData } from "../../gui-shared/src";
|
|
4
4
|
import { plannedHomes, text } from "./app-model";
|
|
5
5
|
import { FileExplorerSurface } from "./FileExplorerSurface";
|
|
6
6
|
import { PathActions } from "./PathActions";
|
|
7
|
-
import { type VaultDialogIntent, VaultPadlock, vaultDialogIntentForStatus } from "./VaultBadges";
|
|
8
7
|
|
|
9
8
|
export { AiProvidersSurface } from "./AiProvidersSurface";
|
|
9
|
+
export { SecuritySurface } from "./SecuritySurface";
|
|
10
|
+
export { LockedVaultGate, VaultSurface } from "./VaultSurface";
|
|
10
11
|
|
|
11
12
|
export function OverviewSurface({ status }: { status: GuiStatusData }) {
|
|
12
13
|
const metrics = [
|
|
13
14
|
{ label: text.setup, value: status.update.restart_required ? "restart" : "current", detail: status.update.installed_version },
|
|
14
15
|
{ label: text.projects, value: String(status.repos.total), detail: status.repos.health },
|
|
15
16
|
{ label: text.config, value: String(status.settings.key_count), detail: status.settings.value_policy },
|
|
16
|
-
{ label: text.security, value: String(status.secrets.length), detail: "secret references" },
|
|
17
|
+
{ label: text.security, value: status.vault.unlocked ? String(status.secrets.length) : "hidden", detail: "secret references" },
|
|
17
18
|
];
|
|
18
19
|
|
|
19
20
|
return (
|
|
@@ -98,173 +99,6 @@ export function ProjectsSurface({ status }: { status: GuiStatusData }) {
|
|
|
98
99
|
);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
export function SecuritySurface({ status }: { status: GuiStatusData }) {
|
|
102
|
-
return (
|
|
103
|
-
<section className="panel" aria-label={text.security}>
|
|
104
|
-
<div className="section-heading">
|
|
105
|
-
<p className="eyebrow">{text.readOnly}</p>
|
|
106
|
-
<h2>{text.security}</h2>
|
|
107
|
-
<p>{text.securityBoundary}</p>
|
|
108
|
-
</div>
|
|
109
|
-
<div className="notice compact-notice" role="note">
|
|
110
|
-
Vault data policy: Provider AI sends approved plaintext context to a third-party provider; Local AI keeps model processing on this device; Hybrid mode must redact or split protected context before any provider call.
|
|
111
|
-
</div>
|
|
112
|
-
<ul className="object-list">
|
|
113
|
-
{status.secrets.map((secret) => (
|
|
114
|
-
<li key={secret.name}>
|
|
115
|
-
<strong>{secret.name}</strong>
|
|
116
|
-
<span>{secret.status}</span>
|
|
117
|
-
</li>
|
|
118
|
-
))}
|
|
119
|
-
</ul>
|
|
120
|
-
</section>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function VaultSurface({ onVaultRequest, status }: { onVaultRequest: (intent: VaultDialogIntent) => void; status: GuiStatusData }) {
|
|
125
|
-
const vault = status.vault;
|
|
126
|
-
const vaultCollection = vault.collections.find((collection) => collection.surface_ids.includes("vault")) ?? vault.collections[0];
|
|
127
|
-
const readiness = [
|
|
128
|
-
{ label: "migration", value: vault.readiness.migration_allowed ? "ready" : "blocked" },
|
|
129
|
-
{ label: "setup", value: vault.readiness.setup_required ? "required" : "done" },
|
|
130
|
-
{ label: "restart test", value: vault.readiness.restart_test_required ? "required" : "verified" },
|
|
131
|
-
{ label: "remote unlock", value: vault.readiness.remote_unlock_enabled ? "enabled" : "disabled" },
|
|
132
|
-
];
|
|
133
|
-
const featureCards = [
|
|
134
|
-
{ label: text.vaultStatus, value: vault.status, detail: "Metadata-only lock state from the local helper." },
|
|
135
|
-
{ label: text.vaultSetup, value: vault.setup_state, detail: vault.setup_hint },
|
|
136
|
-
{ label: text.vaultLockUnlock, value: vault.locked ? "locked" : "unlocked", detail: vault.unlock_hint },
|
|
137
|
-
{ label: text.vaultDevices, value: `${vault.devices.length} device`, detail: "Device trust metadata only; private keys are never exposed." },
|
|
138
|
-
{ label: text.vaultSync, value: vault.sync.status, detail: "Encrypted bundles and signed manifests over untrusted transports." },
|
|
139
|
-
{ label: text.vaultMessages, value: vault.secure_messages.status, detail: "Secure message placeholders keep payloads hidden while locked." },
|
|
140
|
-
{ label: text.vaultBackups, value: vault.backups.status, detail: "Encrypted backups and recovery flows are metadata-only here." },
|
|
141
|
-
{ label: text.vaultAudit, value: vault.audit.status, detail: `${vault.audit.event_count} redacted audit events; ${vault.audit.latest_event_ref}.` },
|
|
142
|
-
];
|
|
143
|
-
|
|
144
|
-
return (
|
|
145
|
-
<section className="surface-page vault-surface" aria-label={text.vault}>
|
|
146
|
-
<div className="hero-panel vault-hero">
|
|
147
|
-
<div className="section-heading split-heading">
|
|
148
|
-
<div>
|
|
149
|
-
<p className="eyebrow">{vault.value_policy}</p>
|
|
150
|
-
<h2>{text.vault}</h2>
|
|
151
|
-
<p>{text.vaultIntro}</p>
|
|
152
|
-
</div>
|
|
153
|
-
{vaultCollection ? <VaultPadlock collection={vaultCollection} onActivate={onVaultRequest} vault={vault} /> : null}
|
|
154
|
-
</div>
|
|
155
|
-
<ul aria-label="Vault readiness" className="vault-readiness-strip">
|
|
156
|
-
{readiness.map((item) => <li key={item.label}><strong>{item.label}</strong>{item.value}</li>)}
|
|
157
|
-
</ul>
|
|
158
|
-
</div>
|
|
159
|
-
{vault.locked ? (
|
|
160
|
-
<div className="notice compact-notice" role="note">
|
|
161
|
-
{text.vaultLockedPreview} {vault.unlock_hint}
|
|
162
|
-
</div>
|
|
163
|
-
) : (
|
|
164
|
-
<div className="notice compact-notice" role="note">
|
|
165
|
-
Vault is unlocked for this local session. Protected actions remain read-only until audited write routes are implemented.
|
|
166
|
-
</div>
|
|
167
|
-
)}
|
|
168
|
-
<div className="vault-card-grid">
|
|
169
|
-
{featureCards.map((card) => <VaultFeatureCard detail={card.detail} key={card.label} label={card.label} value={card.value} />)}
|
|
170
|
-
</div>
|
|
171
|
-
<section className="panel vault-setup-panel" aria-label={text.vaultSetup}>
|
|
172
|
-
<div className="section-heading split-heading">
|
|
173
|
-
<div>
|
|
174
|
-
<p className="eyebrow">{text.vaultSetup}</p>
|
|
175
|
-
<h2>{vault.readiness.setup_required ? "Setup required" : "Setup metadata"}</h2>
|
|
176
|
-
<p>{vault.setup_hint}</p>
|
|
177
|
-
</div>
|
|
178
|
-
<button className="secondary-action vault-cta" onClick={() => onVaultRequest(vaultDialogIntentForStatus(vault))} title={vault.unlock_hint} type="button">{vault.unlocked ? "Lock Vault" : text.vaultUnlockCta}</button>
|
|
179
|
-
</div>
|
|
180
|
-
<ol className="vault-step-list">
|
|
181
|
-
<li>Initialize locally with the hidden-prompt helper.</li>
|
|
182
|
-
<li>Verify the harmless restart test before migrating real data.</li>
|
|
183
|
-
<li>Keep passphrases, recovery material, and private keys out of chat, arguments, environment variables, logs, issues, and fixtures.</li>
|
|
184
|
-
</ol>
|
|
185
|
-
<code>{vault.unlock_hint}</code>
|
|
186
|
-
</section>
|
|
187
|
-
<section className="panel" aria-label="Vault encrypted collections">
|
|
188
|
-
<div className="section-heading">
|
|
189
|
-
<p className="eyebrow">{text.vaultStatus}</p>
|
|
190
|
-
<h2>Encrypted collections</h2>
|
|
191
|
-
<p>{text.vaultCollectionIntro}</p>
|
|
192
|
-
</div>
|
|
193
|
-
<ul className="object-list vault-collection-list">
|
|
194
|
-
{vault.collections.map((collection) => <VaultCollectionRow collection={collection} key={collection.id} onVaultRequest={onVaultRequest} vault={vault} />)}
|
|
195
|
-
</ul>
|
|
196
|
-
</section>
|
|
197
|
-
<section className="panel" aria-label="Vault devices and audit">
|
|
198
|
-
<div className="section-heading split-heading">
|
|
199
|
-
<div>
|
|
200
|
-
<p className="eyebrow">{text.vaultDevices}</p>
|
|
201
|
-
<h2>Devices, sync, messages, backups, and audit</h2>
|
|
202
|
-
<p>These placeholders expose readiness and redacted metadata only. Git, object storage, messaging, SSH, VPNs, and VPS disks remain untrusted transports.</p>
|
|
203
|
-
</div>
|
|
204
|
-
<span className="count-pill">{vault.sync.transport_policy}</span>
|
|
205
|
-
</div>
|
|
206
|
-
<div className="vault-device-grid">
|
|
207
|
-
{vault.devices.map((device) => (
|
|
208
|
-
<article className="vault-device-card" key={device.id_ref}>
|
|
209
|
-
<p className="eyebrow">{device.trust_state}</p>
|
|
210
|
-
<h3>{device.label}</h3>
|
|
211
|
-
<Detail label="device" value={device.id_ref} />
|
|
212
|
-
<Detail label="last seen" value={device.last_seen} />
|
|
213
|
-
<Detail label="audit head" value={device.audit_head_ref} />
|
|
214
|
-
</article>
|
|
215
|
-
))}
|
|
216
|
-
</div>
|
|
217
|
-
</section>
|
|
218
|
-
</section>
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function LockedVaultGate({ collection, label, onVaultRequest, vault }: {
|
|
223
|
-
collection: GuiVaultCollectionSummary;
|
|
224
|
-
label: string;
|
|
225
|
-
onVaultRequest: (intent: VaultDialogIntent) => void;
|
|
226
|
-
vault: GuiVaultStatusData;
|
|
227
|
-
}) {
|
|
228
|
-
return (
|
|
229
|
-
<section className="panel vault-locked-gate" aria-label={`${label} locked by Vault`}>
|
|
230
|
-
<div className="section-heading split-heading">
|
|
231
|
-
<div>
|
|
232
|
-
<p className="eyebrow">{collection.data_class}</p>
|
|
233
|
-
<h2>{label} is locked</h2>
|
|
234
|
-
<p>{text.vaultLockedPreview}</p>
|
|
235
|
-
</div>
|
|
236
|
-
<VaultPadlock collection={collection} onActivate={onVaultRequest} vault={vault} />
|
|
237
|
-
</div>
|
|
238
|
-
<div className="notice compact-notice" role="note">
|
|
239
|
-
{text.vaultTooltip} {vault.unlock_hint}
|
|
240
|
-
</div>
|
|
241
|
-
<button className="secondary-action vault-cta" onClick={() => onVaultRequest(vaultDialogIntentForStatus(vault))} title={vault.unlock_hint} type="button">{text.vaultUnlockCta}</button>
|
|
242
|
-
</section>
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function VaultFeatureCard({ detail, label, value }: { detail: string; label: string; value: string }) {
|
|
247
|
-
return (
|
|
248
|
-
<article className="vault-feature-card">
|
|
249
|
-
<span>{label}</span>
|
|
250
|
-
<strong>{value}</strong>
|
|
251
|
-
<small>{detail}</small>
|
|
252
|
-
</article>
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function VaultCollectionRow({ collection, onVaultRequest, vault }: { collection: GuiVaultCollectionSummary; onVaultRequest: (intent: VaultDialogIntent) => void; vault: GuiVaultStatusData }) {
|
|
257
|
-
return (
|
|
258
|
-
<li>
|
|
259
|
-
<strong>{collection.label}</strong>
|
|
260
|
-
<VaultPadlock collection={collection} compact onActivate={onVaultRequest} vault={vault} />
|
|
261
|
-
<span>{collection.preview_policy}</span>
|
|
262
|
-
<small>{collection.labels.join(", ")}</small>
|
|
263
|
-
<small>{collection.surface_ids.join(", ")}</small>
|
|
264
|
-
</li>
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
102
|
export function LocalReposSurface({ status }: { status: GuiStatusData }) {
|
|
269
103
|
const [activeTab, setActiveTab] = useState<"setup" | "files">("setup");
|
|
270
104
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { GuiVaultStatusData } from "@aidevops/gui-shared";
|
|
2
|
+
import { type ReactElement, type RefObject, useRef } from "react";
|
|
3
|
+
import type { IconType } from "react-icons";
|
|
4
|
+
import { FiAlertTriangle, FiCheckCircle, FiClipboard, FiLock, FiRefreshCw, FiShield, FiTerminal, FiUnlock } from "react-icons/fi";
|
|
5
|
+
import { terminalActionForIntent, useVaultCommandLaunch, useVaultDialogFocus, type VaultLaunchStatus } from "./useVaultAccessDialog";
|
|
6
|
+
import type { VaultDialogIntent } from "./VaultBadges";
|
|
7
|
+
import { vaultCommandText } from "./vault-command-bridge";
|
|
8
|
+
|
|
9
|
+
export { terminalActionForIntent } from "./useVaultAccessDialog";
|
|
10
|
+
|
|
11
|
+
interface VaultAccessModalProps {
|
|
12
|
+
intent: VaultDialogIntent;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
onRefresh: () => Promise<void> | void;
|
|
15
|
+
onTerminalLaunch: () => void;
|
|
16
|
+
vault: GuiVaultStatusData;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function VaultAccessModal({ intent, onClose, onRefresh, onTerminalLaunch, vault }: VaultAccessModalProps): ReactElement {
|
|
20
|
+
const primaryActionRef = useRef<HTMLButtonElement | null>(null);
|
|
21
|
+
const dialogRef = useRef<HTMLElement | null>(null);
|
|
22
|
+
const content = dialogContentFactories[intent](vault);
|
|
23
|
+
const terminalAction = terminalActionForIntent(intent);
|
|
24
|
+
const { launchStatus, launchTerminal } = useVaultCommandLaunch({ intent, onRefresh, onTerminalLaunch });
|
|
25
|
+
useVaultDialogFocus({ dialogRef, intent, onClose, primaryActionRef });
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="vault-modal-backdrop">
|
|
29
|
+
<section
|
|
30
|
+
aria-describedby="vault-access-description"
|
|
31
|
+
aria-labelledby="vault-access-title"
|
|
32
|
+
aria-modal="true"
|
|
33
|
+
className="vault-modal"
|
|
34
|
+
ref={dialogRef}
|
|
35
|
+
role="dialog"
|
|
36
|
+
>
|
|
37
|
+
<header className="vault-modal-header">
|
|
38
|
+
<span className="vault-modal-icon" aria-hidden="true">{dialogIcon(intent)}</span>
|
|
39
|
+
<div><p className="eyebrow">aidevops Vault</p><h2 id="vault-access-title">{content.title}</h2></div>
|
|
40
|
+
</header>
|
|
41
|
+
<div className="vault-modal-body">
|
|
42
|
+
<p id="vault-access-description">{content.detail}</p>
|
|
43
|
+
<div className={`notice compact-notice ${intent === "recover" || intent === "unavailable" ? "warning-notice" : ""}`} role="note">{content.notice}</div>
|
|
44
|
+
{terminalAction === null ? null : <div className="vault-command-preview"><FiTerminal aria-hidden="true" /><code>{vaultCommandText(terminalAction)}</code></div>}
|
|
45
|
+
{/* biome-ignore lint/a11y/useSemanticElements: role=group preserves the existing grid element and requested accessible grouping. */}
|
|
46
|
+
<div className="vault-modal-state-grid" aria-label="Current Vault metadata" role="group">
|
|
47
|
+
<span><strong>Vault</strong>{vault.status}</span><span><strong>Setup</strong>{vault.setup_state}</span><span><strong>Helper</strong>{vault.helper_status}</span>
|
|
48
|
+
</div>
|
|
49
|
+
<VaultLaunchStatusMessage status={launchStatus} />
|
|
50
|
+
</div>
|
|
51
|
+
<VaultModalActions content={content} intent={intent} launchStatus={launchStatus} onClose={onClose} onRefresh={onRefresh} onTerminalLaunch={launchTerminal} primaryActionRef={primaryActionRef} />
|
|
52
|
+
<p className="vault-modal-footnote">This dialog never requests or stores a passphrase. Secret input belongs only in the terminal helper's hidden local prompt.</p>
|
|
53
|
+
</section>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface DialogContent {
|
|
59
|
+
action: string;
|
|
60
|
+
detail: string;
|
|
61
|
+
notice: string;
|
|
62
|
+
title: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const dialogContentFactories: Record<VaultDialogIntent, (vault: GuiVaultStatusData) => DialogContent> = {
|
|
66
|
+
lock: () => ({ action: "Open lock terminal", detail: "Locking forgets in-memory keys and hides protected previews again.", notice: "The fixed local command does not receive browser data or secret material.", title: "Lock local Vault" }),
|
|
67
|
+
recover: () => ({ action: "Open recovery guidance", detail: "Vault metadata appears damaged. Preserve existing encrypted data and review conservative recovery options.", notice: "Do not initialise with --force or overwrite the existing Vault.", title: "Review Vault recovery" }),
|
|
68
|
+
setup: () => ({ action: "Open setup terminal", detail: "Create this device's Vault once through the secure local helper.", notice: "Save the new passphrase in a trusted password manager. aidevops cannot recover it.", title: "Set up Vault" }),
|
|
69
|
+
unavailable: () => ({ action: "Retry status", detail: "Vault readiness is not authoritative, so setup and passphrase actions are disabled.", notice: "Check the local helper and crypto runtime, then retry. Existing encrypted data will not be reinitialised.", title: "Vault status unavailable" }),
|
|
70
|
+
unlock: (vault) => ({ action: "Open secure terminal", detail: "Unlock the existing Vault with the passphrase you already saved.", notice: vault.unlock_hint, title: "Unlock existing Vault" }),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const dialogIcons: Record<VaultDialogIntent, IconType> = {
|
|
74
|
+
lock: FiLock,
|
|
75
|
+
recover: FiAlertTriangle,
|
|
76
|
+
setup: FiShield,
|
|
77
|
+
unavailable: FiAlertTriangle,
|
|
78
|
+
unlock: FiUnlock,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function dialogIcon(intent: VaultDialogIntent): ReactElement {
|
|
82
|
+
const Icon = dialogIcons[intent];
|
|
83
|
+
return <Icon />;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function VaultModalActions({ content, intent, launchStatus, onClose, onRefresh, onTerminalLaunch, primaryActionRef }: {
|
|
87
|
+
content: DialogContent;
|
|
88
|
+
intent: VaultDialogIntent;
|
|
89
|
+
launchStatus: VaultLaunchStatus;
|
|
90
|
+
onClose: () => void;
|
|
91
|
+
onRefresh: () => Promise<void> | void;
|
|
92
|
+
onTerminalLaunch: () => Promise<void>;
|
|
93
|
+
primaryActionRef: RefObject<HTMLButtonElement | null>;
|
|
94
|
+
}): ReactElement {
|
|
95
|
+
const retriesStatus = intent === "unavailable";
|
|
96
|
+
const ActionIcon = retriesStatus ? FiRefreshCw : FiTerminal;
|
|
97
|
+
const className = intent === "lock" || intent === "recover" ? "secondary-action" : "primary-action";
|
|
98
|
+
const runAction = retriesStatus ? onRefresh : onTerminalLaunch;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<footer className="vault-modal-actions">
|
|
102
|
+
<button className="secondary-action" onClick={onClose} type="button">Close</button>
|
|
103
|
+
<button className={className} data-vault-intent={intent} disabled={!retriesStatus && launchStatus === "requesting"} onClick={() => void runAction()} ref={primaryActionRef} type="button"><ActionIcon aria-hidden="true" /> {content.action}</button>
|
|
104
|
+
</footer>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const launchStatusPresentation: Partial<Record<VaultLaunchStatus, { Icon: IconType; className: string; role: "alert" | "status"; text: string }>> = {
|
|
109
|
+
copied: { Icon: FiClipboard, className: "vault-valid", role: "status", text: "Command copied. Run it in your local terminal." },
|
|
110
|
+
failed: { Icon: FiAlertTriangle, className: "vault-invalid", role: "alert", text: "Open a local terminal and run the displayed command." },
|
|
111
|
+
opened: { Icon: FiCheckCircle, className: "vault-valid", role: "status", text: "Secure terminal opened. Return here after the command completes; status refreshes on focus." },
|
|
112
|
+
requesting: { Icon: FiTerminal, className: "vault-valid", role: "status", text: "Requesting the secure local terminal…" },
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function VaultLaunchStatusMessage({ status }: { status: VaultLaunchStatus }): ReactElement | null {
|
|
116
|
+
const presentation = launchStatusPresentation[status];
|
|
117
|
+
if (presentation === undefined) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { Icon, className, role, text } = presentation;
|
|
122
|
+
return <p className={className} role={role}><Icon aria-hidden="true" /> {text}</p>;
|
|
123
|
+
}
|