Skip to content
Merged
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
9 changes: 9 additions & 0 deletions api/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,15 @@ export const flockEvents = {

const kbHandler = (kbInfo) => {
if (kbInfo.type === eventType && kbInfo.event.key.toLowerCase() === key) {
// Shortcut chords (Ctrl+Z undo, ⌘S…) belong to the app/browser, not
// gameplay — without this, undo on a focused canvas walks the player
// ("z" is bound to FORWARD for AZERTY keyboards). Releases still fire
// so a key held before a chord can't get stuck.
if (
!isReleased &&
(kbInfo.event.ctrlKey || kbInfo.event.metaKey || kbInfo.event.altKey)
)
return;
callback();
}
};
Expand Down
3 changes: 3 additions & 0 deletions api/material.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ export const flockMaterial = {
},
glowMesh(mesh, glowColor = null) {
const applyGlow = (m) => {
// Don't glow the say plane.
if (m.name === "textPlane" || m.metadata?.isTextPlane) return;

m.metadata = m.metadata || {};
m.metadata.glow = true;

Expand Down
55 changes: 43 additions & 12 deletions flock.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
callbackMode: true,
separateAnimations: true,
memoryDebug: false,
showErrorBanners: false,
memoryMonitorInterval: 5000,
materialsDebug: false,
meshDebug: false,
Expand Down Expand Up @@ -422,14 +423,14 @@
banner.style.left = "0";
banner.style.right = "0";
banner.style.padding = "12px";
banner.style.background = "#3b0b0b";
banner.style.color = "#ffb3b3";
banner.style.background = "#511d91";
banner.style.color = "#ffffff";
banner.style.fontSize = "16px";
banner.style.fontFamily = "'Asap', sans-serif";
banner.style.zIndex = "20000";
banner.style.textAlign = "center";
banner.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.4)";
banner.style.borderBottom = "2px solid #d33";
banner.style.borderBottom = "2px solid #3a1568";
doc.body.prepend(banner);
},
handlePhysicsOutOfMemory(error) {
Expand Down Expand Up @@ -696,6 +697,10 @@
});
},
showRuntimeErrorBanner(message) {
if (!flock.showErrorBanners) {
flock.console?.error?.(message);
return;
}
const doc = flock.document ?? globalThis.document;
if (!doc?.body) return;
const bannerId = "runtime-error-banner";
Expand All @@ -708,14 +713,14 @@
banner.style.left = "0";
banner.style.right = "0";
banner.style.padding = "12px";
banner.style.background = "#3b0b0b";
banner.style.color = "#ffb3b3";
banner.style.background = "#511d91";
banner.style.color = "#ffffff";
banner.style.fontSize = "16px";
banner.style.fontFamily = "'Asap', sans-serif";
banner.style.zIndex = "20000";
banner.style.textAlign = "center";
banner.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.4)";
banner.style.borderBottom = "2px solid #d33";
banner.style.borderBottom = "2px solid #3a1568";
banner.style.cursor = "pointer";
banner.title = "Click to dismiss";
banner.addEventListener("click", () => banner.remove());
Expand Down Expand Up @@ -769,6 +774,15 @@
sesScript.text = sesText;
doc.head.appendChild(sesScript);

// Re-wraps a host-realm fn into this realm; lockdown only tames this
// realm, so a raw host fn would leak the untamed Function via
// `.constructor` (sandbox escape). Must run before lockdown.
const wrapScript = doc.createElement("script");
wrapScript.type = "text/javascript";
wrapScript.text =
"window.__flockWrapHostFn = (fn) => (...args) => fn(...args);";
doc.head.appendChild(wrapScript);

Comment on lines +777 to +785

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate existing sandbox and runCode tests for an escape-regression test.
fd -HI -t f . | rg '(test|spec)' | while IFS= read -r file; do
  rg -n -C 3 'runCode|Compartment|lockdown|constructor|performance\.now' "$file" || true
done

Repository: flipcomputing/flock

Length of output: 12530


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | sed -n '1,200p'

echo "== flock.js outlines/size =="
if [ -f flock.js ]; then
  wc -l flock.js
  ast-grep outline flock.js --match 'startFlockSession|lockdown|constructor|performance\.now|runCode' || true
  echo "== relevant flock.js lines 720-845 =="
  sed -n '720,845p' flock.js | nl -ba -v720
else
  echo "flock.js not found"
fi

echo "== lockfiles/config for ses and tests =="
git ls-files | rg '(^SECURITY|SECURITY|README|package.(json|lock|json5|workspaces|json$)|test|spec|playwright|vitest|jest|mocha)' || true
rg -n '"(ses|lockdown|sesify|ses-shim|SES)"|runCode|playwright|vitest|jest|mocha' package.json package-lock.json SECURITY README.md 2>/dev/null || true

echo "== runCode mentions in source =="
rg -n -C 3 'runCode|compartment|Compartment|lockdown|escape|sandbox|SES' . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' || true

Repository: flipcomputing/flock

Length of output: 4608


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | sed -n '1,200p'

echo "== flock.js outlines/size =="
if [ -f flock.js ]; then
  wc -l flock.js
  ast-grep outline flock.js --match 'startFlockSession|lockdown||performance\.now|runCode' || true
  echo "== relevant flock.js lines 720-845 =="
  sed -n '720,845p' flock.js | nl -ba -v720
else
  echo "flock.js not found"
fi

echo "== lockfiles/config for ses and tests =="
git ls-files | rg '(^SECURITY|SECURITY|README|package.(json|lock|json5|json$)|test|spec|playwright|vitest|jest|mocha)' || true
rg -n '"(ses|lockdown|sesify|SES)"|runCode|playwright|vitest|jest|mocha' package.json package-lock.json SECURITY README.md 2>/dev/null || true

echo "== runCode mentions in source =="
rg -n -C 3 'runCode|compartment|Compartment|lockdown|escape|sandbox|SES' . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' || true

Repository: flipcomputing/flock

Length of output: 4752


🏁 Script executed:

#!/bin/bash
set -euo firef; set -euo pipefail

echo "== flock lines 720-850 =="
awk 'NR>=720 && NR<=850 { printf "%5d\t%s\n", NR, $0 }' flock.js

echo "== target line snippets =="
python3 - <<'PY'
from pathlib import Path
text = Path("flock.js").read_text()
lines = text.splitlines()
for start,end in [(777,785),(818,820),(829,837)]:
    print(f"\n-- {start}-{end} --")
    for n in range(start, end+1):
        if n <= len(lines):
            print(f"{n}: {lines[n-1]}")
PY

echo "== test/source matches =="
for p in API.md jest.config.* rollup.config.* vite.config.* package.json SECURITY README.md; do
  if [ -e "$p" ]; then echo "-- $p --"; sed -n '1,220p' "$p"; fi
done
rg -n -C 3 'runCode|Compartment|lockdown|constructor|performance\.now|SES|escape|sandbox|test' \
  flock.js test spec .github scripts docs api blocks playground demo 2>/dev/null \
  | sed -n '1,220p' || true

echo "== tests and config files =="
find . -maxdepth 3 -type f \( -name '*test*' -o -name '*spec*' -o -name 'jest.config.*' -o -name 'vitest.config.*' -o -name 'package.json' -o -name 'mocha.opts' -o -name 'SECURITY*' \) \
  -not -path './node_modules/*' \
  -not -path './dev-dist/*' \
  -not -path './dist/*' \
  -not -path './build/*' \
  -print | sort

Repository: flipcomputing/flock

Length of output: 203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== flock lines 720-850 =="
awk 'NR>=720 && NR<=850 { printf "%5d\t%s\n", NR, $0 }' flock.js

echo "== target line snippets =="
python3 - <<'PY'
from pathlib import Path
text = Path("flock.js").read_text()
lines = text.splitlines()
for start,end in [(777,785),(818,820),(829,837)]:
    print(f"\n-- {start}-{end} --")
    for n in range(start, end+1):
        if n <= len(lines):
            print(f"{n}: {lines[n-1]}")
PY

echo "== top-level test/config file references =="
for p in API.md jest.config.* rollup.config.* vite.config.* package.json SECURITY README.md; do
  if [ -e "$p" ]; then echo "-- $p --"; sed -n '1,220p' "$p"; fi
done

echo "== focused matches in tracked text files =="
python3 - <<'PY'
import subprocess
from pathlib import Path
files = subprocess.check_output(["git", "ls-files"], text=True).splitlines()
text_files = [f for f in files if Path(f).suffix in {".js",".mjs",".cjs",".json",".md"}/>]
for f in text_files:
    try:
        text = Path(f).read_text(errors="ignore")
    except Exception:
        continue
    if any(p in text for p in ["runCode","Compartment","lockdown","console.log","performance.now","constructor","SES","escape","sandbox"]):
        hits = [(i+1,line.rstrip()) for i,line in enumerate(text.splitlines()) if any(p in line for p in ["runCode","Compartment","lockdown","console.log","performance.now","constructor","SES","escape","sandbox"])]
        print(f"\n-- {f} hits: {len(hits)} --")
        for i,line in hits[:50]:
            print(f"{i}: {line}")
PY

echo "== tests and config files =="
find . -maxdepth 3 -type f \( -name '*test*' -o -name '*spec*' -o -name 'jest.config.*' -o -name 'vitest.config.*' -o -name 'package.json' -o -name 'mocha.opts' -o -name 'SECURITY*' \) \
  -not -path './node_modules/*' \
  -not -path './dev-dist/*' \
  -not -path './dist/*' \
  -not -path './build/*' \
  -print | sort

Repository: flipcomputing/flock

Length of output: 26446


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== focused matches in tracked text files (fixed) =="
python3 - <<'PY'
import subprocess
from pathlib import Path

files = subprocess.check_output(["git", "ls-files"], text=True).splitlines()
keywords = ["runCode", "Compartment", "lockdown", "console.log", "performance.now", "constructor", "SES", "escape", "sandbox"]
text_files = [f for f in files if Path(f).suffix in {".js", ".mjs", ".cjs", ".json", ".md"}]
for f in text_files:
    try:
        text = Path(f).read_text(errors="ignore")
    except Exception:
        continue
    hits = [(i+1,line.rstrip()) for i,line in enumerate(text.splitlines()) if any(k in line for k in keywords)]
    if hits:
        print(f"\n-- {f} hits: {len(hits)} --")
        for i,line in hits[:200]:
            print(f"{i}: {line}")
PY

echo "== scripts =="
git ls-files scripts | sort | while read -r f; do
  echo "-- $f --"
  sed -n '1,220p' "$f"
done

echo "== tests/html/script mentions =="
git ls-files | rg '(^tests/|\.html$|\.mjs$)' | sort | while read -r f; do
  if rg -q 'runCode|Compartment|lockdown|flock|performance\.now|constructor|CSP|iframe|ses' "$f" 2>/dev/null; then
    echo "-- $f --"
    sed -n '1,240p' "$f" | grep -n -C 3 'runCode\|Compartment\|lockdown\|flock\|performance\.now\|constructor\|iframe\|ses\|CSP\|sandbox' || true
  fi
done

Repository: flipcomputing/flock

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== exact security-related matches in tracked files =="
python3 - <<'PY'
from pathlib import Path
import subprocess
files = subprocess.check_output(["git","ls-files"], text=True).splitlines()
keywords = ["escape regression", "endowment-escape", "endowment escape", "runCode", "Compartment", "lockdown", "performance.now", "constructor", "SES", "__flockWrapHostFn"]
for f in files:
    try:
        text = Path(f).read_text(errors="ignore")
    except Exception:
        continue
    if any(k in text for k in keywords):
        hits = [(i+1, line.rstrip()) for i, line in enumerate(text.splitlines()) if any(k in line for k in keywords)]
        print(f"\n-- {f} hits: {len(hits)} --")
        for n, line in hits[:200]:
            print(f"{n}: {line}")
PY

echo "== tests files for exact terms =="
git ls-files tests scripts **/*.mjs **/*.js 2>/dev/null | xargs rg -n -C 2 'endowment|constructor|runtime|sandbox|escape regression|Compartment|lockdown|performance\.now|runCode|__flockWrapHostFn' || true

echo "== tests directory files =="
find tests scripts -maxdepth 3 -type f 2>/dev/null | sort

Repository: flipcomputing/flock

Length of output: 50376


Add an endowment-escape regression test.

Run untrusted code that obtains a callable endowment with a computed "constructor" property. Verify that it cannot execute generated source or access the host window or document. Test the wrapped function and performance.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 `@flock.js` around lines 777 - 785, Add a regression test covering the
host-function wrapping initialized by wrapScript: run untrusted code with a
callable endowment and computed "constructor" access, and assert it cannot
execute generated source or reach the host window or document. Include checks
for both the wrapped function endowment and performance.now, preserving the
existing lockdown behavior.

Source: Coding guidelines

// lockdown the iframe realm
win.lockdown();

Expand Down Expand Up @@ -801,8 +815,9 @@
for (const [key, value] of Object.entries(whitelist)) {
const t = typeof value;
if (t === "function") {
// Bind to null so we don't leak host `this`
endowments[key] = value.bind(null);
// Wrap into the iframe realm: a host-realm fn leaks the untamed host
// Function via `.constructor` (sandbox escape). bind(null) drops host `this`.
endowments[key] = win.__flockWrapHostFn(value.bind(null));
} else if (value == null || (t !== "object" && t !== "symbol")) {
// primitives only
endowments[key] = value;
Expand All @@ -811,13 +826,15 @@
}
}

endowments.performance = {
now: win.performance.now.bind(win.performance),
};
// win.Object, not a host `{}`: a host literal leaks host Function via
// obj.constructor.constructor.
endowments.performance = new win.Object();
endowments.performance.now = win.performance.now.bind(win.performance);

endowments.requestAnimationFrame = win.requestAnimationFrame.bind(win);

endowments.Date = { now: win.Date.now.bind(win.Date) };
endowments.Date = new win.Object();
endowments.Date.now = win.Date.now.bind(win.Date);

// Undefine unwanted globals
// --- shadow unsafe / unneeded globals ---
Expand Down Expand Up @@ -1323,6 +1340,10 @@
);

flock.canvas.addEventListener("keydown", function (event) {
// Shortcut chords (Ctrl+Z undo, ⌘S…) belong to the app/browser, not
// gameplay — without this, undo on a focused canvas walks the player
// ("z"/"q" are bound to FORWARD/LEFT for AZERTY keyboards).
if (event.ctrlKey || event.metaKey || event.altKey) return;
Comment on lines +1343 to +1346

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify the Babylon.js 8.56.0 camera modifier behavior used by this integration.
curl -fsSL https://raw.githubusercontent.com/BabylonJS/Babylon.js/8.56.0/packages/dev/core/src/Cameras/Inputs/freeCameraKeyboardMoveInput.ts | sed -n '106,158p'
curl -fsSL https://raw.githubusercontent.com/BabylonJS/Babylon.js/8.56.0/packages/dev/core/src/Cameras/Inputs/arcRotateCameraKeyboardMoveInput.ts | sed -n '103,155p'

Repository: flipcomputing/flock

Length of output: 5068


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate flock.js and inspect relevant line range =="
fd -a '^flock\.js$' . | sed 's#^\./##'
FLOCK_PATH="$(fd '^flock\.js$' . | head -n 1)"
wc -l "$FLOCK_PATH"
sed -n '1300,1370p' "$FLOCK_PATH"

echo
echo "== search for Babylon keyboard/input setup in flock.js =="
rg -n "onKeyboardObservable|keyboard|camera|FreeCamera|ArcRotate|Canvas|addEventListener|addEventListener\\(" "$FLOCK_PATH" -C 3

echo
echo "== search package metadata for Babylon version/source path =="
rg -n "babylon|`@babylonjs`|Babylon|version" package.json package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null || true

Repository: flipcomputing/flock

Length of output: 50375


Filter Ctrl and Alt chords before Babylon camera inputs update _keys.

This canvas keydown returns before Babylon’s keyboard observers. Babylon.js 8.56.0 ignores Meta, but FreeCamera and ArcRotateCamera still add Ctrl- and Alt-modified mapped keys; use a focused canvas with Ctrl/Alt plus movement keys to show it. Preserve the application shortcut handling separately from the camera-input filtering.

🤖 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 `@flock.js` around lines 1343 - 1346, Separate application shortcut handling
from Babylon camera-input filtering in the canvas keydown handler. Ensure Ctrl-
or Alt-modified keys are blocked from reaching Babylon keyboard observers and
updating camera _keys, while application/browser shortcuts continue to be
handled normally; preserve Meta behavior as intended.

flock.canvas.currentKeyPressed = event.key;
flock.canvas.pressedKeys.add(event.key);
});
Expand Down Expand Up @@ -1350,6 +1371,16 @@
flock._hardResetCameraControls(flock.scene?.activeCamera);
});

// macOS suppresses keyup for keys released while ⌘ is held, so both
// Babylon's camera keyboard input (_keys) and our pressedKeys set would
// keep them held until the next blur. Clear both when ⌘ comes up.
flock.canvas.addEventListener("keyup", (e) => {
if (e.key !== "Meta") return;
const kb = flock.scene?.activeCamera?.inputs?.attached?.keyboard;
if (kb?._keys) kb._keys.length = 0;
flock.canvas.pressedKeys?.clear();
});

flock.engineReady = true;
},
setupGamepadCameraControls() {
Expand Down Expand Up @@ -1605,15 +1636,15 @@
md.heightmapBody._pluginData.hpBodyId,
);
}
} catch (e) {

Check failure on line 1639 in flock.js

View workflow job for this annotation

GitHub Actions / eslint

'e' is defined but never used
/* ignore */
}
try {
md.heightmapBody?.dispose();
} catch {}

Check failure on line 1644 in flock.js

View workflow job for this annotation

GitHub Actions / eslint

Empty block statement
try {
md.heightmapShape?.dispose();
} catch {}

Check failure on line 1647 in flock.js

View workflow job for this annotation

GitHub Actions / eslint

Empty block statement
md.heightmapBody = null;
md.heightmapShape = null;
}
Expand Down
2 changes: 1 addition & 1 deletion generators/generators-material.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function registerMaterialGenerators(javascriptGenerator) {
// Hex colour -------------------------------------------------
javascriptGenerator.forBlock["colour_from_string"] = function (block) {
const colourValue = block.getFieldValue("COLOR") || "#000000";
return [`"${colourValue}"`, javascriptGenerator.ORDER_ATOMIC];
return [JSON.stringify(colourValue), javascriptGenerator.ORDER_ATOMIC];
};

// Set material of object -------------------------------------
Expand Down
25 changes: 25 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,31 @@ <h2 id="modal-title" data-i18n="about_heading">
<div id="fullscreen-desc" class="sr-only">
Toggle between fullscreen and windowed view of the application
</div>
<div
id="canvasStoppedOverlay"
class="canvas-stopped-overlay"
hidden
>
<button
type="button"
class="canvas-stopped-overlay__play"
id="overlayPlayButton"
data-i18n="canvas_overlay_play"
data-i18n-attrs="aria-label,title"
aria-label="Press Play to start"
title="Press Play to start"
>
<span class="icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
<path
fill="currentColor"
d="M0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zM188.3 147.1c-7.6 4.2-12.3 12.3-12.3 20.9l0 176c0 8.7 4.7 16.7 12.3 20.9s16.8 4.1 24.3-.5l144-88c7.1-4.4 11.5-12.1 11.5-20.5s-4.4-16.1-11.5-20.5l-144-88c-7.4-4.5-16.7-4.7-24.3-.5z"
/>
</svg>
</span>
</button>
</div>
<aside
class="gizmo-buttons"
id="gizmoButtons"
Expand Down
1 change: 1 addition & 0 deletions locale/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,7 @@ export default {
contrast_theme_ui: "Kontrast",
run_code_button_ui: "Code ausführen",
stop_code_button_ui: "Code stoppen",
canvas_overlay_play_ui: "Zum Starten auf Play drücken", // ai
open_button_ui: "Projekt von Datei öffnen",
open_file_input_label_ui: "Projektdatei zum Öffnen auswählen",
export_code_button_ui: "Projekt speichern",
Expand Down
1 change: 1 addition & 0 deletions locale/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,7 @@ export default {

run_code_button_ui: "Run your code",
stop_code_button_ui: "Stop your code",
canvas_overlay_play_ui: "Press Play to start",
open_button_ui: "Open a project from a file on your computer",
open_file_input_label_ui: "Select project file to open",
export_code_button_ui: "Save this project to a file on your computer.",
Expand Down
1 change: 1 addition & 0 deletions locale/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,7 @@ export default {

run_code_button_ui: "Ejecutar tu código", // human
stop_code_button_ui: "Detener tu código", // human
canvas_overlay_play_ui: "Pulsa Play para empezar", // ai
open_button_ui: "Abrir un proyecto desde un archivo en tu computadora", // human
open_file_input_label_ui: "Selecciona el archivo de proyecto para abrir", // human
export_code_button_ui:
Expand Down
1 change: 1 addition & 0 deletions locale/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,7 @@ export default {

run_code_button_ui: "Exécuter votre code",
stop_code_button_ui: "Arrêter votre code",
canvas_overlay_play_ui: "Appuyez sur Lecture pour démarrer", // ai
open_button_ui: "Ouvrir un projet depuis un fichier sur votre ordinateur",
open_file_input_label_ui: "Sélectionnez le fichier projet à ouvrir",
export_code_button_ui:
Expand Down
1 change: 1 addition & 0 deletions locale/it.js
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,7 @@ export default {

run_code_button_ui: "Esegui il tuo codice",
stop_code_button_ui: "Ferma il tuo codice",
canvas_overlay_play_ui: "Premi Play per iniziare", // ai
open_button_ui: "Apri un progetto da un file sul tuo computer",
open_file_input_label_ui: "Seleziona il file di progetto da aprire",
export_code_button_ui: "Salva questo progetto in un file sul tuo computer.",
Expand Down
1 change: 1 addition & 0 deletions locale/pl.js
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,7 @@ export default {

run_code_button_ui: "Uruchom kod",
stop_code_button_ui: "Zatrzymaj kod",
canvas_overlay_play_ui: "Naciśnij Play, aby rozpocząć", // ai
open_button_ui: "Otwórz projekt z pliku na komputerze",
open_file_input_label_ui: "Wybierz plik projektu do otwarcia",
export_code_button_ui: "Zapisz projekt do pliku na komputerze",
Expand Down
1 change: 1 addition & 0 deletions locale/pt.js
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,7 @@ export default {

run_code_button_ui: "Executar o teu código",
stop_code_button_ui: "Parar o teu código",
canvas_overlay_play_ui: "Pressiona Play para começar", // ai
open_button_ui: "Abrir um projeto a partir de um ficheiro no teu computador",
open_file_input_label_ui: "Selecione o arquivo de projeto para abrir",
export_code_button_ui: "Guardar este projeto num ficheiro no teu computador.",
Expand Down
1 change: 1 addition & 0 deletions locale/sv.js
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,7 @@ export default {

run_code_button_ui: "Kör din kod",
stop_code_button_ui: "Stoppa din kod",
canvas_overlay_play_ui: "Tryck på Spela för att börja", // ai
open_button_ui: "Öppna ett projekt från en fil på din dator",
open_file_input_label_ui: "Välj projektfil att öppna",
export_code_button_ui: "Spara detta projekt till en fil på din dator.",
Expand Down
54 changes: 54 additions & 0 deletions main/execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export async function executeCode(options = {}) {
// Set the flag to indicate the function is running
isExecuting = true;

// Remove the "press play" overlay now that the scene is starting
hideStoppedOverlay();

// Utility function for delay
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

Expand Down Expand Up @@ -74,13 +77,64 @@ export async function executeCode(options = {}) {
isExecuting = false;
}

function getStoppedOverlay() {
return document.getElementById("canvasStoppedOverlay");
}

let overlayResizeHandler = null;

// Size the overlay to the rendered canvas box so it covers only the Babylon
// canvas, not the surrounding canvas area or the (hidden) gizmo buttons.
function positionStoppedOverlay() {
const overlay = getStoppedOverlay();
const canvas = document.getElementById("renderCanvas");
if (!overlay || !canvas) return;
overlay.style.left = `${canvas.offsetLeft}px`;
overlay.style.top = `${canvas.offsetTop}px`;
overlay.style.width = `${canvas.offsetWidth}px`;
overlay.style.height = `${canvas.offsetHeight}px`;
}

function showStoppedOverlay() {
const overlay = getStoppedOverlay();
if (!overlay) return;
// Hides the gizmo buttons (via CSS) and covers the canvas; the canvas is made
// inert below so screen reader and keyboard users skip the frozen scene.
document.getElementById("canvasArea")?.classList.add("is-stopped");
const canvas = document.getElementById("renderCanvas");
if (canvas) canvas.inert = true;
positionStoppedOverlay();
overlay.hidden = false;
// Move keyboard focus to the play button so it can be triggered immediately
overlay.querySelector("#overlayPlayButton")?.focus({ preventScroll: true });
if (!overlayResizeHandler) {
overlayResizeHandler = () => positionStoppedOverlay();
window.addEventListener("resize", overlayResizeHandler);
}
}

function hideStoppedOverlay() {
const overlay = getStoppedOverlay();
if (overlay) overlay.hidden = true;
document.getElementById("canvasArea")?.classList.remove("is-stopped");
const canvas = document.getElementById("renderCanvas");
if (canvas) canvas.inert = false;
if (overlayResizeHandler) {
window.removeEventListener("resize", overlayResizeHandler);
overlayResizeHandler = null;
}
}

export function stopCode() {
flock.stopAllSounds();

// Stop rendering
flock.engine.stopRenderLoop();
//console.log("Render loop stopped.");

// Show the "press play" overlay over the (now frozen) canvas
showStoppedOverlay();

// Remove event listeners
flock.removeEventListeners();

Expand Down
4 changes: 4 additions & 0 deletions main/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,10 @@ function parseProjectJsonResponse(response) {
);
}

if (projectText.length > 4 * 1024 * 1024) {
throw new Error("File content is too large");
}
Comment on lines +380 to +382

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate file map ---'
ast-grep outline main/files.js --match '$_' --view concise 2>/dev/null | head -200 || true

printf '%s\n' '--- relevant source ---'
sed -n '320,420p' main/files.js

printf '%s\n' '--- response and size-limit usages ---'
rg -n -C 4 'response\.text|projectText\.length|File content is too large|Content-Length|fetch\(' main test tests 2>/dev/null || true

printf '%s\n' '--- repository test/config files ---'
git ls-files | rg '(^|/)(package\.json|.*test.*|.*spec.*|.*jest.*|.*vitest.*|.*playwright.*|.*cypress.*)' | head -200

Repository: flipcomputing/flock

Length of output: 9942


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- package and API test setup ---'
cat package.json
sed -n '1,240p' scripts/run-api-tests.mjs
sed -n '1,220p' playwright.config.js

printf '%s\n' '--- source context and test references ---'
sed -n '430,480p' main/files.js
rg -n -C 3 'fetchProjectJson|project=|File content is too large|file_too_large_alert|loadWorkspace' tests scripts test-*.js .github 2>/dev/null | head -240

printf '%s\n' '--- tracked change summary ---'
git diff --stat
git status --short

Repository: flipcomputing/flock

Length of output: 11807


🏁 Script executed:

#!/bin/bash
set -eu

node - <<'JS'
const LIMIT = 4 * 1024 * 1024;
const cases = [
  { name: "ASCII exactly at limit", text: "a".repeat(LIMIT) },
  { name: "ASCII one byte over", text: "a".repeat(LIMIT + 1) },
  { name: "2-byte UTF-8 exactly 4 MiB", text: "\u00e9".repeat(LIMIT / 2) },
  { name: "2-byte UTF-8 one byte over", text: "\u00e9".repeat(LIMIT / 2 + 1) },
  { name: "4-byte UTF-8 exactly 4 MiB", text: "\u{1f600}".repeat(LIMIT / 4) },
  { name: "4-byte UTF-8 one byte over", text: "\u{1f600}".repeat(Math.floor(LIMIT / 4) + 1) },
];

for (const { name, text } of cases) {
  const utf8Bytes = new TextEncoder().encode(text).byteLength;
  console.log(JSON.stringify({
    name,
    utf16CodeUnits: text.length,
    utf8Bytes,
    currentGuardRejects: text.length > LIMIT,
    byteLimitRejects: utf8Bytes > LIMIT,
  }));
}
JS

Repository: flipcomputing/flock

Length of output: 958


Enforce the size limit before buffering the response.

response.text() reads and decodes the complete response before this check runs. A large project response can consume substantial memory before the code throws. projectText.length counts UTF-16 code units, so multibyte content can exceed 4 MiB without triggering the check.

If this is a 4 MiB response limit, reject a known-large Content-Length and count bytes while reading response.body, aborting after 4 MiB. Add tests for exactly 4 MiB, 4 MiB plus one byte, and multibyte content.

🤖 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 `@main/files.js` around lines 380 - 382, Replace the response.text() buffering
and projectText.length check with byte-based streaming in the surrounding
project response flow: reject Content-Length values over 4 MiB, read
response.body incrementally, track encoded byte counts, and abort once the limit
is exceeded while preserving successful decoding at exactly 4 MiB. Add coverage
for exactly 4 MiB, one byte over, and multibyte content.


try {
return JSON.parse(projectText);
} catch (error) {
Expand Down
7 changes: 7 additions & 0 deletions main/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,13 @@ function initializeApp() {

runCodeButton.addEventListener("click", executeCode);
stopCodeButton.addEventListener("click", stopCode);
// Clicking anywhere on the stopped overlay (not just the play button) starts
// the scene — matches the pointer cursor shown across the whole overlay. The
// inner button's click bubbles up here, and executeCode's isExecuting guard
// makes the resulting second call a no-op.
Comment on lines +237 to +240

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct or remove the new comments.

The new comments do not consistently describe the current implementation. Remove provenance-only comments.

  • main/main.js#L237-L240: Remove the claim that a button click causes a second executeCode call.
  • style.css#L438-L438: Remove or correct the claim that gizmos remain clickable while stopped.
  • locale/de.js#L1072-L1072: Remove // ai.
  • locale/es.js#L1066-L1066: Remove // ai.
  • locale/fr.js#L1065-L1065: Remove // ai.
  • locale/it.js#L1067-L1067: Remove // ai.
  • locale/pl.js#L1065-L1065: Remove // ai.
  • locale/pt.js#L1057-L1057: Remove // ai.
  • locale/sv.js#L1047-L1047: Remove // ai.

As per coding guidelines, “Keep comments infrequent and include them only when genuinely noteworthy” and “Comments must reflect only the current state of the code.”

📍 Affects 9 files
  • main/main.js#L237-L240 (this comment)
  • style.css#L438-L438
  • locale/de.js#L1072-L1072
  • locale/es.js#L1066-L1066
  • locale/fr.js#L1065-L1065
  • locale/it.js#L1067-L1067
  • locale/pl.js#L1065-L1065
  • locale/pt.js#L1057-L1057
  • locale/sv.js#L1047-L1047
🤖 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 `@main/main.js` around lines 237 - 240, Remove the inaccurate provenance
comments: in main/main.js lines 237-240 remove the claim about a button click
causing a second executeCode call; in style.css line 438 remove or correct the
claim that gizmos remain clickable while stopped; and in locale/de.js line 1072,
locale/es.js line 1066, locale/fr.js line 1065, locale/it.js line 1067,
locale/pl.js line 1065, locale/pt.js line 1057, and locale/sv.js line 1047
remove the standalone “// ai” comments. Keep only comments that accurately
describe the current implementation.

Source: Coding guidelines

document
.getElementById("canvasStoppedOverlay")
?.addEventListener("click", executeCode);
exportCodeButton.addEventListener("click", exportCode);

// Make open button work with keyboard
Expand Down
Loading
Loading