Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,10 @@ VITE_SUPABASE_ANON_KEY=your-anon-key
# Google Identity Services β€” Web OAuth client ID (public)
VITE_GOOGLE_WEB_CLIENT_ID=your-web-client-id.apps.googleusercontent.com

# PostHog analytics (optional β€” skip in local dev)
VITE_POSTHOG_API_KEY=phc_your_project_token
VITE_POSTHOG_API_HOST=https://us.i.posthog.com
# For production with reverse proxy: VITE_POSTHOG_API_HOST=https://e.bayanflow.com

# Optional: Pyodide CDN override (see src/constants/pyodideCdn.js)
# VITE_PYODIDE_CDN_BASE=
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ jobs:
env:
VITE_GIT_BRANCH: ${{ github.ref_name }}
VITE_DEV_SITE_URL: 'https://dev.bayanflow.com'
VITE_POSTHOG_API_KEY: ${{ secrets.VITE_POSTHOG_API_KEY }}
VITE_POSTHOG_API_HOST: 'https://us.i.posthog.com'

- name: Upload build artifacts
uses: actions/upload-artifact@v5
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/deploy-cloudflare.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ jobs:
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
VITE_GOOGLE_WEB_CLIENT_ID: ${{ secrets.VITE_GOOGLE_WEB_CLIENT_ID }}
VITE_POSTHOG_API_KEY: ${{ secrets.VITE_POSTHOG_API_KEY }}
VITE_POSTHOG_API_HOST: https://e.bayanflow.com
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/preview-cloudflare.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ jobs:
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
VITE_GOOGLE_WEB_CLIENT_ID: ${{ secrets.VITE_GOOGLE_WEB_CLIENT_ID }}
VITE_POSTHOG_API_KEY: ${{ secrets.VITE_POSTHOG_API_KEY }}
VITE_POSTHOG_API_HOST: https://e.dev.bayanflow.com

- name: Upload preview version
id: deploy
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Bayan Flow implements several security measures:

- **Client-side only**: No server-side data processing or storage for algorithm execution
- **Local algorithm execution**: Sorting, searching, pathfinding, and graph visualizations run entirely in the browser
- **Limited third-party calls**: Umami analytics (privacy-oriented), GitHub public API (release/repo metadata), and jsDelivr (Pyodide, only when the user opens the Python panel)
- **Limited third-party calls**: PostHog analytics (privacy-oriented, via first-party proxy), GitHub public API (release/repo metadata), and jsDelivr (Pyodide, only when the user opens the Python panel)
- **HTTP security headers** (via Cloudflare Workers static assets `public/_headers`): Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy
- **Content Security Policy**: Restricts script, connect, frame, and worker sources to approved origins
- **Automated scanning**:
Expand Down
28 changes: 0 additions & 28 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,34 +38,6 @@
})();
</script>

<!-- Umami Analytics - Dynamically loaded based on environment -->
<script>
(function () {
const hostname = window.location.hostname;
let websiteId;
if (
hostname === 'dev.bayanflow.com' ||
hostname.slice(-12) === '.workers.dev'
) {
// Develop
websiteId = 'b21ffc5d-66c1-4bfc-9e54-e05dbbe26852';
} else if (
hostname === 'bayanflow.com' ||
hostname === 'www.bayanflow.com'
) {
// main
websiteId = 'c096589e-f026-47e9-b458-aeb6850d5d5c';
}
if (websiteId) {
const script = document.createElement('script');
script.defer = true;
script.src = 'https://cloud.umami.is/script.js';
script.setAttribute('data-website-id', websiteId);
document.head.appendChild(script);
}
})();
</script>

<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://bayanflow.com/" />
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@monaco-editor/react": "^4.7.0",
"@octokit/rest": "^22.0.1",
"@phosphor-icons/react": "^2.1.10",
"@posthog/react": "^1.10.3",
"@remotion/media": "4.0.436",
"@remotion/web-renderer": "^4.0.436",
"@supabase/supabase-js": "^2.108.2",
Expand All @@ -69,6 +70,7 @@
"i18next": "^25.7.1",
"i18next-browser-languagedetector": "^8.2.0",
"isomorphic-dompurify": "^3.18.0",
"posthog-js": "^1.404.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-i18next": "^15.7.4",
Expand Down
81 changes: 81 additions & 0 deletions pnpm-lock.yaml

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

2 changes: 1 addition & 1 deletion public/_headers
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), identity-credentials-get=(self "https://accounts.google.com")
# Pyodide CDN origins: jsDelivr below; custom VITE_PYODIDE_CDN_BASE origins appended at build (vite.config.js)
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' blob: https://cloud.umami.is https://static.cloudflareinsights.com https://cdn.jsdelivr.net https://accounts.google.com; connect-src 'self' blob: https://cloud.umami.is https://gateway.umami.is https://cloudflareinsights.com https://api.github.com https://cdn.jsdelivr.net https://www.remotion.pro https://qketsapzqpzmccljfjcm.supabase.co https://accounts.google.com https://oauth2.googleapis.com; img-src 'self' data: blob: https://api.producthunt.com https://lh3.googleusercontent.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; font-src 'self' data:; worker-src 'self' blob:; media-src 'self' blob:; frame-src https://www.youtube-nocookie.com https://accounts.google.com; object-src 'none'; base-uri 'self'; form-action 'self'
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' blob: https://*.posthog.com https://static.cloudflareinsights.com https://cdn.jsdelivr.net https://accounts.google.com; connect-src 'self' blob: https://*.posthog.com https://cloudflareinsights.com https://api.github.com https://cdn.jsdelivr.net https://www.remotion.pro https://qketsapzqpzmccljfjcm.supabase.co https://accounts.google.com https://oauth2.googleapis.com; img-src 'self' data: blob: https://api.producthunt.com https://lh3.googleusercontent.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; font-src 'self' data:; worker-src 'self' blob:; media-src 'self' blob:; frame-src https://www.youtube-nocookie.com https://accounts.google.com; object-src 'none'; base-uri 'self'; form-action 'self'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

Keep the runtime CSP and build-time assertion aligned for the first-party proxy.

PostHog traffic is routed through https://e.bayanflow.com, but the runtime header omits that origin and the assertion only validates *.posthog.com. This blocks proxy requests in browsers while allowing the CSP test to pass.

  • public/_headers#L8-L8: add https://e.bayanflow.com to connect-src.
  • scripts/cspHeaders.js#L89-L106: require the same proxy origin in the analytics CSP assertion.

As per coding guidelines: β€œPreserve the CSP requirements for Supabase, Google profile images, and Google Identity Services resources; CSP is asserted by scripts/cspHeaders.js at build time.”

πŸ“ Affects 2 files
  • public/_headers#L8-L8 (this comment)
  • scripts/cspHeaders.js#L89-L106
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/_headers` at line 8, Add https://e.bayanflow.com to connect-src in
public/_headers while preserving the existing Supabase and other resource
sources. Update the analytics CSP assertion in scripts/cspHeaders.js, including
its relevant validation symbols, to require the same proxy origin alongside the
existing PostHog origin checks; both sites must remain aligned.

Source: Coding guidelines

Link: <https://bayanflow.com/sitemap.xml>; rel="sitemap"
Link: </.well-known/api-catalog>; rel="api-catalog"
Link: </.well-known/agent-card.json>; rel="service-desc"
Expand Down
26 changes: 26 additions & 0 deletions scripts/cspHeaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,32 @@ function getSupabaseOrigin() {
}
}

/**
* Assert CSP directives required for PostHog analytics.
* @param {string} csp
* @param {string} source - Label for error messages (e.g. "public/_headers")
* @returns {{ scriptSrc: string; connectSrc: string }}
*/
export function assertAnalyticsCspDirectives(csp, source) {
const directives = parseCspDirectives(csp);

const scriptSrc = directives.get('script-src');
if (!scriptSrc?.includes('https://*.posthog.com')) {
throw new Error(
`${source}: script-src must include https://*.posthog.com (PostHog SDK)`
);
}

const connectSrc = directives.get('connect-src');
if (!connectSrc?.includes('https://*.posthog.com')) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
throw new Error(
`${source}: connect-src must include https://*.posthog.com (PostHog analytics)`
);
}

return { scriptSrc: scriptSrc ?? '', connectSrc: connectSrc ?? '' };
}

/**
* Assert CSP directives required for Supabase auth and Google profile avatars.
* @param {string} csp
Expand Down
2 changes: 2 additions & 0 deletions src/components/SignInPromptModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next';
import { GoogleLogo } from '@phosphor-icons/react';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth } from '../hooks/useAuth';
import { trackSignInClicked } from '../services/analyticsEvents';

const LEGACY_FEATURES = new Set([
'code',
Expand Down Expand Up @@ -57,6 +58,7 @@ function SignInPromptModal({ feature, isOpen, onClose, metadata = {} }) {
}, [isOpen]);

const handleSignIn = async () => {
trackSignInClicked('modal');
setIsSigningIn(true);
setSignInError(false);
try {
Expand Down
2 changes: 2 additions & 0 deletions src/components/UserMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import UserAvatar from './UserAvatar';
import Tooltip from './ui/Tooltip';
import { trackSignInClicked } from '../services/analyticsEvents';

/**
* @param {Object} props
Expand Down Expand Up @@ -62,6 +63,7 @@ function UserMenu({ variant = 'landing', hideAvatar = false }) {
}

const handleSignIn = async () => {
trackSignInClicked('navbar');
setIsSigningIn(true);
setSignInError(false);
try {
Expand Down
6 changes: 3 additions & 3 deletions src/content/legal/privacy.en.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const PRIVACY_POLICY_SECTIONS = [
'We designed Bayan Flow to minimize data collection. Depending on how you use the Service, the following may apply:',
],
list: [
'Website analytics (Umami): We use Umami, a privacy-oriented analytics tool, on production and development deployments. A small inline script in the page selects the correct Umami website ID for each hostname. Umami collects aggregate usage metrics such as pages visited, referrer, browser type, and device type. Umami does not use advertising cookies. Umami may assign a hashed visitor identifier for counting unique visits; we treat this as pseudonymous data and disclose it for transparency. We do not use a cookie consent banner for Umami because it is configured for privacy-friendly, non-advertising analytics.',
'Website analytics (PostHog): We use PostHog, a privacy-oriented analytics and product platform, on production and development deployments. PostHog collects aggregate usage metrics such as pages visited, referrer, browser type, device type, and user interactions with features like algorithm visualizations. PostHog uses a first-party proxy (e.bayanflow.com) to avoid ad-blocker interference. PostHog may assign a distinct visitor identifier; we treat this as pseudonymous data and disclose it for transparency. Session replay is enabled to help us understand and improve the user experience; text and media inputs are masked for privacy. We do not use a cookie consent banner for PostHog because it is configured for privacy-friendly, non-advertising analytics.',
'Server logs (Cloudflare): Our hosting provider, Cloudflare Workers, automatically records standard edge access logs, which may include your IP address, browser user agent, requested URL, and timestamp. See Cloudflare’s privacy documentation at https://www.cloudflare.com/privacypolicy/.',
'Local storage on your device: Preferences such as theme, language, sound settings, Python panel layout, custom Python test cases, swipe tutorial state, and full-screen mode are stored in your browser’s localStorage. This data stays on your device and is not transmitted to our servers.',
'GitHub API: The header badge and footer may request public release metadata and repository statistics from GitHub (api.github.com). Those requests are made to GitHub and may expose standard network metadata such as your IP address and user agent to GitHub.',
Expand Down Expand Up @@ -55,7 +55,7 @@ export const PRIVACY_POLICY_SECTIONS = [
id: 'retention',
title: 'Retention',
paragraphs: [
'Analytics data retention follows Umami’s configuration for our project.',
'Analytics data retention follows PostHog’s configuration for our project.',
'Server logs are retained according to Cloudflare’s policies.',
'Sign-up IP addresses on user profiles are retained for up to 90 days, then anonymized. Signup audit events used for rate limiting are deleted after 7 days. When you delete your account, sign-up IP data and related audit rows are removed.',
'localStorage data remains on your device until you clear it through your browser settings.',
Expand All @@ -65,7 +65,7 @@ export const PRIVACY_POLICY_SECTIONS = [
id: 'sharing',
title: 'Sharing and Processors',
paragraphs: [
'We do not sell personal data. We use service providers to host and measure the Service, including Cloudflare (hosting), Umami (analytics), and Supabase (optional authentication and profile storage). Third-party resources (GitHub, jsDelivr, YouTube/Google) are loaded only as described above.',
'We do not sell personal data. We use service providers to host and measure the Service, including Cloudflare (hosting), PostHog (analytics and product insights), and Supabase (optional authentication and profile storage). Third-party resources (GitHub, jsDelivr, YouTube/Google) are loaded only as described above.',
],
},
{
Expand Down
4 changes: 2 additions & 2 deletions src/content/legal/privacy.en.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ describe('privacy.en', () => {
}
});

it('discloses Umami, localStorage, and optional Google sign-in', () => {
it('discloses PostHog, localStorage, and optional Google sign-in', () => {
const body = PRIVACY_POLICY_SECTIONS.flatMap(section => [
...section.paragraphs,
...(section.list ?? []),
]).join(' ');

expect(body).toMatch(/Umami/i);
expect(body).toMatch(/PostHog/i);
expect(body).toMatch(/Cloudflare/i);
expect(body).toMatch(/GitHub.*IP address|api\.github\.com.*IP address/i);
expect(body).toMatch(/localStorage/i);
Expand Down
Loading
Loading