Skip to content

Commit d0abb4b

Browse files
authored
Merge pull request #225 from OpenKnots/okcode/blur-env-file-values
Add blur protection for .env file contents
2 parents 3d5fe8f + b29cc02 commit d0abb4b

2 files changed

Lines changed: 83 additions & 12 deletions

File tree

apps/server/src/workspaceEntries.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,23 @@ async function mapWithConcurrency<TInput, TOutput>(
686686
return results;
687687
}
688688

689+
const ENV_FILE_PATTERN = /^\.env(\..+)?$/;
690+
691+
/**
692+
* Discover .env* files at the workspace root so they appear in the file tree
693+
* even when gitignored. Only root-level env files are included.
694+
*/
695+
async function discoverEnvFiles(cwd: string): Promise<string[]> {
696+
try {
697+
const dirEntries = await fs.readdir(cwd, { withFileTypes: true });
698+
return dirEntries
699+
.filter((entry) => entry.isFile() && ENV_FILE_PATTERN.test(entry.name))
700+
.map((entry) => entry.name);
701+
} catch {
702+
return [];
703+
}
704+
}
705+
689706
async function isInsideGitWorkTree(cwd: string): Promise<boolean> {
690707
const insideWorkTree = await runProcess("git", ["rev-parse", "--is-inside-work-tree"], {
691708
cwd,
@@ -799,6 +816,14 @@ async function buildWorkspaceIndexFromGit(cwd: string): Promise<WorkspaceIndex |
799816
.filter((entry) => entry.length > 0 && !isPathInIgnoredDirectory(entry));
800817
const filePaths = await filterGitIgnoredPaths(cwd, listedPaths);
801818

819+
// Include .env* files that may be gitignored so they appear in the file tree
820+
const envFiles = await discoverEnvFiles(cwd);
821+
for (const envPath of envFiles) {
822+
if (!filePaths.includes(envPath)) {
823+
filePaths.push(envPath);
824+
}
825+
}
826+
802827
const directorySet = new Set<string>();
803828
for (const filePath of filePaths) {
804829
for (const directoryPath of directoryAncestorsOf(filePath)) {

apps/web/src/components/CodeViewerPanel.tsx

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useQuery } from "@tanstack/react-query";
2-
import { FileCodeIcon, XIcon } from "lucide-react";
3-
import { memo, useCallback } from "react";
2+
import { EyeIcon, EyeOffIcon, FileCodeIcon, XIcon } from "lucide-react";
3+
import { memo, useCallback, useState } from "react";
44

55
import { useCodeViewerStore, type CodeViewerTab } from "~/codeViewerStore";
66
import { useTheme } from "~/hooks/useTheme";
@@ -13,6 +13,12 @@ import { MarkdownPreview } from "./MarkdownPreview";
1313
import { isElectron } from "~/env";
1414
import { Button } from "./ui/button";
1515

16+
/** Check if a file path is a dotenv / secrets file whose values should be masked. */
17+
function isEnvFile(filePath: string): boolean {
18+
const basename = filePath.split("/").pop() ?? filePath;
19+
return /^\.env(\..*)?$/.test(basename);
20+
}
21+
1622
export function CodeViewerTabStrip(props: {
1723
tabs: CodeViewerTab[];
1824
activeTabPath: string | null;
@@ -66,6 +72,9 @@ export const CodeViewerFileContent = memo(function CodeViewerFileContent(props:
6672
resolvedTheme: "light" | "dark";
6773
onAddContext: (ctx: CodeContextSelection) => void;
6874
}) {
75+
const envFile = isEnvFile(props.relativePath);
76+
const [envValuesRevealed, setEnvValuesRevealed] = useState(false);
77+
6978
const query = useQuery(
7079
projectReadFileQueryOptions({
7180
cwd: props.cwd,
@@ -109,22 +118,59 @@ export const CodeViewerFileContent = memo(function CodeViewerFileContent(props:
109118
}
110119

111120
return (
112-
<div className="min-h-0 flex-1 overflow-y-auto">
121+
<div className="relative min-h-0 flex-1 overflow-y-auto">
113122
{query.data.truncated && (
114123
<div className="border-b border-amber-500/30 bg-amber-500/10 px-3 py-1 text-[11px] text-amber-700 dark:text-amber-300/90">
115124
File is larger than 1MB. Showing truncated content.
116125
</div>
117126
)}
118-
{isMarkdownPreviewFilePath(props.relativePath) ? (
119-
<MarkdownPreview contents={query.data.contents} />
120-
) : (
121-
<CodeMirrorViewer
122-
contents={query.data.contents}
123-
filePath={props.relativePath}
124-
resolvedTheme={props.resolvedTheme}
125-
onAddContext={props.onAddContext}
126-
/>
127+
128+
{/* Env file: show/hide toggle banner */}
129+
{envFile && (
130+
<div className="sticky top-0 z-10 flex items-center justify-between border-b border-amber-500/30 bg-amber-500/10 px-3 py-1.5">
131+
<span className="text-[11px] font-medium text-amber-700 dark:text-amber-300/90">
132+
Sensitive file — values are hidden by default
133+
</span>
134+
<Button
135+
type="button"
136+
size="xs"
137+
variant="ghost"
138+
className="gap-1.5 text-[11px] text-amber-700 hover:text-amber-900 dark:text-amber-300/90 dark:hover:text-amber-100"
139+
onClick={() => setEnvValuesRevealed((prev) => !prev)}
140+
>
141+
{envValuesRevealed ? (
142+
<>
143+
<EyeOffIcon className="size-3.5" />
144+
Hide values
145+
</>
146+
) : (
147+
<>
148+
<EyeIcon className="size-3.5" />
149+
Show values
150+
</>
151+
)}
152+
</Button>
153+
</div>
127154
)}
155+
156+
<div
157+
className={cn(
158+
envFile &&
159+
!envValuesRevealed &&
160+
"select-none blur-[6px] transition-[filter] duration-200",
161+
)}
162+
>
163+
{isMarkdownPreviewFilePath(props.relativePath) ? (
164+
<MarkdownPreview contents={query.data.contents} />
165+
) : (
166+
<CodeMirrorViewer
167+
contents={query.data.contents}
168+
filePath={props.relativePath}
169+
resolvedTheme={props.resolvedTheme}
170+
onAddContext={props.onAddContext}
171+
/>
172+
)}
173+
</div>
128174
</div>
129175
);
130176
});

0 commit comments

Comments
 (0)