Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 65 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ websockets = ">= 12, < 16"
writer-sdk = ">= 2.3.1, < 3"
python-multipart = ">=0.0.7, < 1"
orjson = "^3.11.0, <4"
sentry-sdk = {extras = ["fastapi"], version = "^2.15.0"}

[tool.poetry.group.build]
optional = true
Expand Down
5 changes: 5 additions & 0 deletions src/ui/.env
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to use config.js. You can take a look at this PR which does that: https://github.com/WriterInternal/fs.agent-flow/pull/89 .

IMO, it needs to be a new hook like

function useWriterConfiguration(core: Core) {
  onMounted(() => {
    if (wf.isWriterCloudApp.value) fetchConfig()
  });

  return config;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
VITE_SENTRY_DSN=https://6c13c3260114c5b1ceb3b6ae58d0cdb3@o1026471.ingest.us.sentry.io/4510398008393728
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify .env file is not committed to version control.

The Sentry DSN is exposed in this .env file. While the DSN is a public key, environment files should typically not be committed to version control as they can contain sensitive configuration. Ensure this file is properly listed in .gitignore, or consider using environment variables at runtime instead.


🏁 Script executed:

#!/bin/bash
# Check if .env is in .gitignore
grep -r "^\.env$" .gitignore

# Check if this .env file is tracked by git
git ls-files src/ui/.env

Length of output: 11


Let me verify the .gitignore status to ensure we have complete information:


---

🏁 Script executed:

```shell
#!/bin/bash
# Check .gitignore content directly
cat .gitignore | grep -E "\.env"

Length of output: 42



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for .gitignore files anywhere in repo
fd -name .gitignore

# Also check if there's a root-level gitignore (alternative names)
ls -la | grep -i git

Length of output: 407



---

🏁 Script executed:

```shell
#!/bin/bash
# Check .gitignore content for .env entries
cat .gitignore

Length of output: 425


Remove src/ui/.env from version control and add it to .gitignore.

The .env file containing the Sentry DSN is currently tracked by git and is not listed in .gitignore. This violates security best practices and exposes environment configuration in version control history.

Add .env to .gitignore and remove the file from git tracking using git rm --cached src/ui/.env. Consider using a .env.example template instead for documenting required environment variables.

🤖 Prompt for AI Agents
In src/ui/.env at line 1, the committed .env contains a Sentry DSN and must be
removed from version control; add src/ui/.env (or just .env) to .gitignore, run
git rm --cached src/ui/.env to stop tracking the file while keeping it locally,
commit the .gitignore change and the removal, and optionally add a
src/ui/.env.example with placeholder variable names (e.g. VITE_SENTRY_DSN=) to
document required environment variables.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Sentry DSN (Data Source Name) is hardcoded and committed to version control. This credential should be treated as a secret. Anyone with repository access can view this DSN, and it remains in git history even if removed in future commits. While Sentry DSNs are designed for client-side use (write-only), exposing them allows malicious actors to send false error reports to your project, potentially causing rate limit issues and polluting your error tracking data.

Committing environment-specific configuration files violates the principle of separating configuration from code. This DSN appears to be for production, which means developers might accidentally send development errors to production Sentry.

Remediation:

Add .env to .gitignore and create .env.example with placeholder values:

VITE_SENTRY_DSN=your_sentry_dsn_here
VITE_SENTRY_ENABLED=true

Configure actual values through your deployment platform's environment variables (Vercel, Netlify, AWS, etc.). Remove .env from git history using tools like BFG Repo-Cleaner. Consider rotating the exposed DSN in Sentry settings.

🔺 Vulnerability (Error)

Image of Graham C Graham C

VITE_SENTRY_ENABLED=true
VITE_SENTRY_ENVIRONMENT=production
VITE_SENTRY_TRACES_SAMPLE_RATE=1.0
VITE_SENTRY_REPLAY_SAMPLE_RATE=0.1
3 changes: 2 additions & 1 deletion src/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"vega-embed": "^6.22.1",
"vega-lite": "^5.7.1",
"vue": "^3.5.0",
"vue-dompurify-html": "^5.0.1"
"vue-dompurify-html": "^5.0.1",
"@sentry/vue": "^10.25.0"
},
"devDependencies": {
"@types/google.maps": "3.55.5",
Expand Down
2 changes: 2 additions & 0 deletions src/ui/src/composables/useBlueprintNodeTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,5 @@ export function useBlueprintNodeTools(
hasToolsButNoFunctionTools,
};
}


70 changes: 70 additions & 0 deletions src/ui/src/composables/useGlobalErrorHandling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { trackError } from "@/observability/frontendMetrics";

let isInitialized = false;
let errorHandlerRef: ((event: ErrorEvent) => void) | null = null;
let rejectionHandlerRef: ((event: PromiseRejectionEvent) => void) | null = null;

function extractError(error: unknown): Error {
if (error instanceof Error) {
return error;
}
if (typeof error === "string") {
return new Error(error);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extractError function converts error strings to Error objects without validating or limiting message length. According to OWASP input validation guidelines, all external input should have maximum length constraints to prevent resource exhaustion. Excessively long error messages can cause memory issues, exceed API payload limits when sent to Sentry, and consume unnecessary storage in logs.

Error messages might include stack traces, large JSON payloads, or extensive debugging information. Without length limits, this creates a DoS vector where triggered errors with arbitrarily large messages could exhaust resources.

Remediation:

Implement maximum length validation:

const MAX_ERROR_LENGTH = 1500;

function extractError(error: unknown): Error {
    if (error instanceof Error) {
        if (error.message.length > MAX_ERROR_LENGTH) {
            error.message = error.message.substring(0, MAX_ERROR_LENGTH) + '... (truncated)';
        }
        return error;
    }
    
    let msg = typeof error === "string" ? error : String(error || "Unknown error");
    if (msg.length > MAX_ERROR_LENGTH) {
        msg = msg.substring(0, MAX_ERROR_LENGTH) + '... (truncated)';
    }
    return new Error(msg);
}
🔸 Vulnerability (Warning)

Image of Graham C Graham C

}
return new Error(String(error || "Unknown error"));
}

function handleError(event: ErrorEvent): void {
try {
const error = extractError(event.error || event.message);
trackError(error, error.name || "window_error");
} catch (trackingError) {
console.error("Failed to track error:", trackingError);
}
}

function handleUnhandledRejection(event: PromiseRejectionEvent): void {
try {
const error = extractError(event.reason);
trackError(error, "unhandled_promise_rejection");
} catch (trackingError) {
console.error("Failed to track promise rejection:", trackingError);
}
}

export function setupGlobalErrorHandling(): void {
if (typeof window === "undefined") {
return;
}

if (isInitialized) {
console.warn("Global error handling already initialized");
return;
}

errorHandlerRef = handleError;
rejectionHandlerRef = handleUnhandledRejection;

window.addEventListener("error", errorHandlerRef);
window.addEventListener("unhandledrejection", rejectionHandlerRef);

isInitialized = true;
}

export function teardownGlobalErrorHandling(): void {
if (typeof window === "undefined" || !isInitialized) {
return;
}

if (errorHandlerRef) {
window.removeEventListener("error", errorHandlerRef);
errorHandlerRef = null;
}

if (rejectionHandlerRef) {
window.removeEventListener("unhandledrejection", rejectionHandlerRef);
rejectionHandlerRef = null;
}

isInitialized = false;
}
35 changes: 30 additions & 5 deletions src/ui/src/composables/useLogger.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,40 @@
/* eslint-disable no-console */

import { observabilityRegistry } from "@/observability";

export type ILogger = Pick<typeof console, "log" | "warn" | "info" | "error">;

/**
* A simple abstraction to use logger in the application. For the moment, it's just a proxy to `console`, but it can be plugged to any library later.
*/
export function useLogger(): ILogger {
const provider = observabilityRegistry.getInitializedProvider();

return {
log: console.log,
warn: console.warn,
info: console.info,
error: console.error,
warn: (...args: any[]) => {
console.warn(...args);
if (provider && args.length > 0) {
const message =
typeof args[0] === "string" ? args[0] : String(args[0]);
provider.captureMessage(message, "warning", {
source: "logger",
component: "useLogger",
args: args.slice(1),
});
}
},
error: (...args: any[]) => {
console.error(...args);
if (provider && args.length > 0) {
const error =
args[0] instanceof Error
? args[0]
: new Error(String(args[0]));
provider.captureException(error, {
source: "logger",
component: "useLogger",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error method passes all additional arguments to the observability provider as context without sanitization. Developers often include user objects, request payloads, or application state when logging errors. If any of this context contains passwords, API keys, tokens, PII, or other sensitive data, it will be sent to Sentry and accessible to all team members.

This violates data protection regulations and creates a security liability. Once sensitive data enters Sentry, it may be retained for extended periods with less stringent access controls than production databases.

Remediation:

Sanitize arguments before sending to observability:

const SENSITIVE = /password|secret|token|key|auth|api[-_]?key/i;

function sanitizeArgs(args: any[]): any[] {
    return args.slice(0, 5).map(arg => {
        if (typeof arg === 'string') {
            return arg.substring(0, 500);
        }
        if (arg && typeof arg === 'object') {
            const clean: any = {};
            for (const [k, v] of Object.entries(arg).slice(0, 20)) {
                clean[k] = SENSITIVE.test(k) ? '[REDACTED]' : v;
            }
            return clean;
        }
        return arg;
    });
}

Apply the same sanitization to the warn method at line 21.

🔸 Vulnerability (Warning)

Image of Graham C Graham C

args: args.slice(1),
});
}
},
};
}
14 changes: 14 additions & 0 deletions src/ui/src/composables/useWriterTracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import type { generateCore } from "@/core";
import { useWriterApi } from "./useWriterApi";
import { watch } from "vue";
import { useLogger } from "./useLogger";
import {
MetricUnit,
incrementMetricSafely,
} from "@/observability/frontendMetrics";

let isIdentified = false;

Expand Down Expand Up @@ -185,6 +189,16 @@ export function useWriterTracking(wf: ReturnType<typeof generateCore>) {
expandEventPropertiesWithResources(properties);
logger.log("[tracking]", eventNameFormated, propertiesExpanded);

incrementMetricSafely(
`user_action.${eventName}`,
{
tags: {
event_type: eventName,
},
unit: MetricUnit.None,
},
);

return await Promise.all([
trackWithApi(eventNameFormated, propertiesExpanded),
trackWithFullStory(eventNameFormated, propertiesExpanded),
Expand Down
Loading