Skip to content

fix: resolve CSP violations blocking Tone.js, Monaco, and Umami; defer AudioContext creation - #188

Merged
ayoub3bidi merged 9 commits into
developfrom
fix/csp-tonejs-sound-init
Jun 27, 2026
Merged

fix: resolve CSP violations blocking Tone.js, Monaco, and Umami; defer AudioContext creation#188
ayoub3bidi merged 9 commits into
developfrom
fix/csp-tonejs-sound-init

Conversation

@ayoub3bidi

@ayoub3bidi ayoub3bidi commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Description

Three CSP directives were missing entries required by production dependencies, causing Tone.js AudioWorklet initialization to fail (AbortError), the Monaco editor code panel to render unstyled, and Umami analytics to drop events. Additionally, Tone.js eagerly created an AudioContext at module-import time, producing autoplay policy warnings.

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)

Changes Made

  • public/_headers — Added blob: to script-src and connect-src for Tone.js v15 AudioWorklet modules; added https://gateway.umami.is to connect-src for Umami Cloud data ingestion; added https://cdn.jsdelivr.net to style-src for Monaco editor CDN CSS.
  • netlify.toml — Same CSP updates for the legacy Netlify deployment.
  • src/utils/masterChain.js — Replaced static import * as Tone from "tone" with lazy async getTone() using dynamic import("tone") so no AudioContext is created at module-load time.
  • src/utils/soundManager.js — Same lazy import pattern; Tone.* references in _buildInstruments(), enable(), and arpeggiate() now resolve via getTone() after user gesture.

Testing

  • All existing tests pass (pnpm test:run) — 119 files, 1563 tests
  • ESLint passes (pnpm lint)
  • Prettier formatting applied (pnpm format:check)
  • Production build succeeds (pnpm build)
  • Built dist/_headers verified for correct CSP directives

Test Results

 Test Files  119 passed (119)
      Tests  1563 passed (1563)

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • My changes generate no new warnings
  • New and existing unit tests pass locally with my changes
  • Base branch is develop

Related Issues

Fixes CSP violations that broke: Tone.js sound system, Monaco editor code panel styling, and Umami analytics tracking on the deployed environment.

Summary by CodeRabbit

  • Bug Fixes
    • Updated the app’s Content Security Policy to allow the required script-src and connect-src sources (including blob: and analytics endpoints) to prevent blocked scripts/connections.
    • Improved audio startup and reliability by lazily loading the audio engine (cached after first use) before building instruments or enabling playback.
    • Fixed arpeggiator scheduling to use the properly initialized audio engine timing.
  • Tests
    • Updated sound manager tests to match the new lazy audio-loading behavior.

…r AudioContext creation

- Add blob: to script-src and connect-src for Tone.js v15 AudioWorklet
- Add gateway.umami.is to connect-src for Umami Cloud analytics
- Add cdn.jsdelivr.net to style-src for Monaco editor CDN CSS
- Replace static Tone.js import with lazy dynamic import to prevent eager AudioContext creation at module load time
@netlify

netlify Bot commented Jun 27, 2026

Copy link
Copy Markdown

Deploy Preview for dev-bayanflow ready!

Name Link
🔨 Latest commit 3d5f02f
🔍 Latest deploy log https://app.netlify.com/projects/dev-bayanflow/deploys/6a3fa1180eb8e10008a2fe52
😎 Deploy Preview https://deploy-preview-188--dev-bayanflow.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Updates Content-Security-Policy header values, changes a Cloudflare preview comment heading, and switches Tone.js access in audio utilities to cached dynamic imports with async call sites.

Changes

CSP and preview config

Layer / File(s) Summary
Updated CSP source lists
netlify.toml, public/_headers
Content-Security-Policy values are updated in both header sources; the Netlify policy adds blob: and additional Umami/Cloudflare origins, and the static headers policy revises its script-src and connect-src source lists.
Preview comment text
.github/workflows/preview-cloudflare.yml
The Cloudflare Workers staging preview comment heading is changed to a Markdown title.

Tone lazy loading

Layer / File(s) Summary
Lazy loader setup
src/utils/masterChain.js, src/utils/soundManager.js
getTone() and the module cache are added for lazy Tone loading, and soundManager.js switches to those shared helpers.
Runtime Tone access
src/utils/masterChain.js, src/utils/soundManager.js, src/utils/soundManager.test.js
createMasterChain, _buildInstruments, enable, and arpeggiate await or read the dynamically loaded Tone module, and the test mock adds matching Tone/time access.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ayoub3bidi/bayan-flow#183: Also changes Cloudflare-hosting configuration, including the preview workflow comment and CSP-related files.
  • ayoub3bidi/bayan-flow#187: Modifies the same public/_headers Content-Security-Policy directives, including script-src, connect-src, and blob: handling.

Suggested labels

config

Poem

I hopped through the headers with a twitch of my nose,
Then cached up Tone so the music could flows.
Blob: in the breeze and async in my toes,
This rabbit says “load later!” and off I goes. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: CSP fixes and deferring Tone.js AudioContext creation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/csp-tonejs-sound-init

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown

Preview for Bayan Flow Staging ready!

Name Link
🔨 Latest commit 3d5f02f
🔍 Latest deploy log https://github.com/ayoub3bidi/bayan-flow/actions/runs/28286103400
😎 Deploy Preview https://pr-188-bayan-flow-staging.ayoub3bidi.workers.dev
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

Preview alias pr-188 on the staging worker. Updates automatically with new commits.

@codecov

codecov Bot commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.47368% with 29 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/workers/pyodide.worker.js 0.00% 12 Missing ⚠️
src/hooks/usePythonExecution.js 69.69% 10 Missing ⚠️
src/components/OutputConsole.jsx 22.22% 7 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/utils/soundManager.js (1)

177-192: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Making arpeggiate async breaks the temporary-volume window.

arpeggiate is invoked through the synchronous withTemporaryVolume(synth, volumeDb, playFn) (lines 143-153), which sets the boosted volume, calls playFn(), then immediately restores the previous volume. Now that arpeggiate is async, playFn() returns at the first await getTone() and the volume is reset before any triggerAttackRelease runs (the body resumes on a later microtask). All arpeggiated milestone/accent sounds (playSorted, playPassComplete, playPathFound, playTargetFound, playNoResult, playCycle, playComponentComplete) therefore play at base volume instead of the adjusted volume. The returned promise is also never awaited, so any rejection is unhandled.

Since enable() (line 112) and _buildInstruments() (line 58) both await getTone() before any playback path runs, _Tone is guaranteed loaded by the time arpeggiate executes — keep it synchronous and read _Tone directly to preserve the volume window.

🐛 Keep arpeggiate synchronous
-  async arpeggiate(
+  arpeggiate(
     synth,
     notes,
     { ascending = true, noteDuration = MILESTONE_NOTE_DURATION } = {}
   ) {
-    const Tone = await getTone();
     const ordered = ascending ? notes : [...notes].reverse();
-    const now = Tone.now();
+    const now = _Tone.now();
🤖 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 `@src/utils/soundManager.js` around lines 177 - 192, The async change in
arpeggiate breaks withTemporaryVolume because playFn returns before the notes
are scheduled, so restore-volume runs too early and any rejection becomes
unhandled. Keep arpeggiate synchronous by using the already-initialized _Tone
directly instead of awaiting getTone(), since enable and _buildInstruments
guarantee Tone is loaded before the playback methods call it. Make the fix in
arpeggiate and verify the callers like playSorted, playPassComplete, and
playTargetFound still benefit from the temporary volume boost.
🧹 Nitpick comments (1)
src/utils/masterChain.js (1)

13-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate lazy-loader across modules.

_Tone + getTone() are defined identically here and in src/utils/soundManager.js (lines 18-25). Each module gets its own cache, so Tone may be resolved twice and the logic is duplicated. Since soundManager.js already imports from this module, consider exporting getTone here and reusing it.

♻️ Share a single loader
 let _Tone = null;

-async function getTone() {
+export async function getTone() {
   if (!_Tone) {
     _Tone = await import('tone');
   }
   return _Tone;
 }

Then in soundManager.js, import it instead of redeclaring:

-let _Tone = null;
-
-async function getTone() {
-  if (!_Tone) {
-    _Tone = await import('tone');
-  }
-  return _Tone;
-}
+import { createMasterChain, getTone } from './masterChain.js';
🤖 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 `@src/utils/masterChain.js` around lines 13 - 20, Duplicate lazy-loading logic
for Tone is defined in both masterChain.js and soundManager.js, which creates
separate caches and unnecessary duplication. Move the shared loader to the
getTone function in masterChain.js by exporting it there, then update
soundManager.js to import and reuse that single helper instead of redeclaring
its own _Tone cache. Keep the existing lazy import behavior intact while
ensuring both modules share the same resolved Tone instance.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@netlify.toml`:
- Line 21: The Content-Security-Policy in netlify.toml is still narrower than
the policy in public/_headers, so update the CSP string to match the same
allowed sources. In particular, adjust the script-src and connect-src entries in
the netlify.toml CSP so they include the missing Cloudflare Insights and
jsDelivr endpoints referenced by the deployed app, keeping the policy consistent
with the header-based configuration.

---

Outside diff comments:
In `@src/utils/soundManager.js`:
- Around line 177-192: The async change in arpeggiate breaks withTemporaryVolume
because playFn returns before the notes are scheduled, so restore-volume runs
too early and any rejection becomes unhandled. Keep arpeggiate synchronous by
using the already-initialized _Tone directly instead of awaiting getTone(),
since enable and _buildInstruments guarantee Tone is loaded before the playback
methods call it. Make the fix in arpeggiate and verify the callers like
playSorted, playPassComplete, and playTargetFound still benefit from the
temporary volume boost.

---

Nitpick comments:
In `@src/utils/masterChain.js`:
- Around line 13-20: Duplicate lazy-loading logic for Tone is defined in both
masterChain.js and soundManager.js, which creates separate caches and
unnecessary duplication. Move the shared loader to the getTone function in
masterChain.js by exporting it there, then update soundManager.js to import and
reuse that single helper instead of redeclaring its own _Tone cache. Keep the
existing lazy import behavior intact while ensuring both modules share the same
resolved Tone instance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d8ceb916-31d5-48de-a8c7-8fdfbb845330

📥 Commits

Reviewing files that changed from the base of the PR and between fe9612d and bfeb803.

📒 Files selected for processing (4)
  • netlify.toml
  • public/_headers
  • src/utils/masterChain.js
  • src/utils/soundManager.js

Comment thread netlify.toml Outdated
Add missing https://static.cloudflareinsights.com and https://cdn.jsdelivr.net to script-src, and https://cloudflareinsights.com to connect-src in netlify.toml CSP to match the header-based policy.
Use pre-loaded _Tone directly instead of async getTone(), since _buildInstruments guarantees Tone is loaded before playback callbacks fire.
Move getTone() and _Tone cache into masterChain.js with named exports so both modules resolve the same Tone instance. Update soundManager.test.js mock to provide getTone and _Tone.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/utils/soundManager.js (1)

111-120: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reset the manager state when lazy init fails.

isEnabled is set before ensureInstrumentsAsync() succeeds, and a rejection leaves initPromise cached forever. Because src/pages/VisualizerApp.jsx:255-278 skips reattaching resume listeners when soundManager.getIsEnabled() is already true, one failed Tone/init path now wedges sound until reload; later runWhenReady() calls also keep chaining onto the same rejected promise.

Suggested fix
  async enable() {
    const Tone = await getTone();
    if (Tone.context.state !== 'running') {
      await Tone.start();
    }
-    this.isEnabled = true;
     this.microEventCounters = {};
     this.melodicStepCounter = 0;
-    await this.ensureInstrumentsAsync();
+    try {
+      await this.ensureInstrumentsAsync();
+      this.isEnabled = true;
+    } catch (error) {
+      this.isEnabled = false;
+      this.initPromise = null;
+      throw error;
+    }
  }
🤖 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 `@src/utils/soundManager.js` around lines 111 - 120, The SoundManager enable
flow leaves the manager marked as enabled before ensureInstrumentsAsync()
succeeds, so a failed lazy init can permanently cache a rejected initPromise and
block later retries. Update enable() in SoundManager to only set isEnabled and
reset counters after ensureInstrumentsAsync() completes, and on any rejection
clear or reset the initPromise/internal state so subsequent runWhenReady() calls
can retry cleanly. Also ensure the VisualizerApp resume-listener path that
checks getIsEnabled() can recover after a failed init instead of treating the
manager as already ready.
🤖 Prompt for all review comments with 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.

Outside diff comments:
In `@src/utils/soundManager.js`:
- Around line 111-120: The SoundManager enable flow leaves the manager marked as
enabled before ensureInstrumentsAsync() succeeds, so a failed lazy init can
permanently cache a rejected initPromise and block later retries. Update
enable() in SoundManager to only set isEnabled and reset counters after
ensureInstrumentsAsync() completes, and on any rejection clear or reset the
initPromise/internal state so subsequent runWhenReady() calls can retry cleanly.
Also ensure the VisualizerApp resume-listener path that checks getIsEnabled()
can recover after a failed init instead of treating the manager as already
ready.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 466d5f1c-76c5-4e23-8559-74feb12eb2ac

📥 Commits

Reviewing files that changed from the base of the PR and between 086d988 and 6279a73.

📒 Files selected for processing (1)
  • src/utils/soundManager.js

@github-actions github-actions Bot added the tests label Jun 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/utils/soundManager.test.js`:
- Around line 24-25: The mock in soundManager.test.js is splitting getTone() and
_Tone across different instances, so update the Tone stub used by the test to
keep both symbols on the same cached mock module. Adjust the test setup around
getTone and _Tone so masterChain.js exercises the shared-instance contract
consistently and can catch bugs related to reusing the same Tone object.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cd17e58b-6e5a-4a66-8798-12387b06c45a

📥 Commits

Reviewing files that changed from the base of the PR and between 6279a73 and 7716359.

📒 Files selected for processing (3)
  • src/utils/masterChain.js
  • src/utils/soundManager.js
  • src/utils/soundManager.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/utils/masterChain.js
  • src/utils/soundManager.js

Comment thread src/utils/soundManager.test.js Outdated
@github-actions github-actions Bot added the ci Workflows label Jun 27, 2026
enable() no longer sets isEnabled=true before ensureInstrumentsAsync() completes. On failure, initPromise and instruments are reset so retries work cleanly instead of being permanently blocked by a cached rejection.
Use a shared mutable object that getTone() populates via Object.assign from the global Tone mock, so _Tone and getTone() return the same instance — matching the real masterChain.js contract.
…gress bar

- Add 'wasm-unsafe-eval' to script-src CSP in public/_headers & netlify.toml
- Split timeouts: 60s for Pyodide init, 10s for code execution
- Worker sends heartbeat during loadPyodide() to prevent false timeouts
- Replace loading spinner with indeterminate motion progress bar
- Run button shows 'Loading Python...' vs 'Running...' distinctly
- Clear stale timeouts on re-run (fixes double-click bug)
@github-actions github-actions Bot added the style Improve styling, design, and animation label Jun 27, 2026
@ayoub3bidi ayoub3bidi self-assigned this Jun 27, 2026
@ayoub3bidi
ayoub3bidi merged commit d88dc58 into develop Jun 27, 2026
15 checks passed
@ayoub3bidi
ayoub3bidi deleted the fix/csp-tonejs-sound-init branch June 27, 2026 10:11
@ayoub3bidi ayoub3bidi mentioned this pull request Jul 16, 2026
35 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Workflows style Improve styling, design, and animation tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant