mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-05-08 06:04:44 +00:00
add: add env masking extension for .env files
Introduce a new extension for CodeMirror that masks sensitive values in .env files. This ensures that sensitive information like API keys or passwords is not displayed in plain text within the editor. The extension dynamically applies masking to values in lines matching the KEY=VALUE format, improving security during development.
This commit is contained in:
parent
95dcd0261a
commit
4b0eaf25ce
@ -25,6 +25,7 @@ import { BinaryContent } from './BinaryContent';
|
||||
import { getTheme, reconfigureTheme } from './cm-theme';
|
||||
import { indentKeyBinding } from './indent';
|
||||
import { getLanguage } from './languages';
|
||||
import { createEnvMaskingExtension } from './EnvMasking';
|
||||
|
||||
const logger = createScopedLogger('CodeMirrorEditor');
|
||||
|
||||
@ -134,6 +135,9 @@ export const CodeMirrorEditor = memo(
|
||||
|
||||
const [languageCompartment] = useState(new Compartment());
|
||||
|
||||
// Add a compartment for the env masking extension
|
||||
const [envMaskingCompartment] = useState(new Compartment());
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const viewRef = useRef<EditorView>();
|
||||
const themeRef = useRef<Theme>();
|
||||
@ -214,6 +218,7 @@ export const CodeMirrorEditor = memo(
|
||||
if (!doc) {
|
||||
const state = newEditorState('', theme, settings, onScrollRef, debounceScroll, onSaveRef, [
|
||||
languageCompartment.of([]),
|
||||
envMaskingCompartment.of([]),
|
||||
]);
|
||||
|
||||
view.setState(state);
|
||||
@ -236,6 +241,7 @@ export const CodeMirrorEditor = memo(
|
||||
if (!state) {
|
||||
state = newEditorState(doc.value, theme, settings, onScrollRef, debounceScroll, onSaveRef, [
|
||||
languageCompartment.of([]),
|
||||
envMaskingCompartment.of([createEnvMaskingExtension(() => docRef.current?.filePath)]),
|
||||
]);
|
||||
|
||||
editorStates.set(doc.filePath, state);
|
||||
|
80
app/components/editor/codemirror/EnvMasking.ts
Normal file
80
app/components/editor/codemirror/EnvMasking.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { EditorView, Decoration, type DecorationSet, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||
|
||||
// Create a proper WidgetType class for the masked text
|
||||
class MaskedTextWidget extends WidgetType {
|
||||
constructor(private readonly _value: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
eq(other: MaskedTextWidget) {
|
||||
return other._value === this._value;
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const span = document.createElement('span');
|
||||
span.textContent = '*'.repeat(this._value.length);
|
||||
span.className = 'cm-masked-text';
|
||||
|
||||
return span;
|
||||
}
|
||||
|
||||
ignoreEvent() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function createEnvMaskingExtension(getFilePath: () => string | undefined) {
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.buildDecorations(view);
|
||||
}
|
||||
|
||||
update(update: { docChanged: boolean; view: EditorView; viewportChanged: boolean }) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
buildDecorations(view: EditorView) {
|
||||
const filePath = getFilePath();
|
||||
const isEnvFile = filePath?.endsWith('.env') || filePath?.includes('.env.') || filePath?.includes('/.env');
|
||||
|
||||
if (!isEnvFile) {
|
||||
return Decoration.none;
|
||||
}
|
||||
|
||||
const decorations: any[] = [];
|
||||
const doc = view.state.doc;
|
||||
|
||||
for (let i = 1; i <= doc.lines; i++) {
|
||||
const line = doc.line(i);
|
||||
const text = line.text;
|
||||
|
||||
// Match lines with KEY=VALUE format
|
||||
const match = text.match(/^([^=]+)=(.+)$/);
|
||||
|
||||
if (match && !text.trim().startsWith('#')) {
|
||||
const [, key, value] = match;
|
||||
const valueStart = line.from + key.length + 1;
|
||||
|
||||
// Create a decoration that replaces the value with asterisks
|
||||
decorations.push(
|
||||
Decoration.replace({
|
||||
inclusive: true,
|
||||
widget: new MaskedTextWidget(value),
|
||||
}).range(valueStart, line.to),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Decoration.set(decorations);
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { json, type ActionFunctionArgs } from '@remix-run/node';
|
||||
import { json, type ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
try {
|
||||
|
Loading…
Reference in New Issue
Block a user