Apply Bswup improvements (#12408)#12418
Conversation
WalkthroughThis PR adds an error UI and retry wiring, hardens and validates the service-worker manifest and install flow (with retries and strict/lax modes), updates fetch/passive caching behavior, adds update polling and coordinated reloads across tabs, expands logging/options, and updates documentation. ChangesBit.Bswup Service Worker Error Handling & Update Polling
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (1 warning, 2 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts`:
- Around line 47-57: The manifest validation detects errors (MANIFEST_ERRORS
from validateAssetsManifest) but the code continues to run and later
dereferences self.assetsManifest.assets, causing a crash; after calling
sendError you should stop further startup: return/throw to prevent asset
enumeration, and guard any access to self.assetsManifest.assets (e.g., use
Array.isArray(self.assetsManifest.assets) before iterating) and ensure VERSION
fallback is computed safely (use (self.assetsManifest && typeof
self.assetsManifest.version === 'string') ? self.assetsManifest.version :
'0.0.0-invalid-manifest') so the service worker does not proceed when the
manifest is invalid; update the block around MANIFEST_ERRORS, sendError,
VERSION, and any asset-enumeration code that uses self.assetsManifest.assets to
enforce this early exit and safe checks.
In `@src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts`:
- Around line 321-324: The catch block in checkForUpdate currently calls
handle(BswupMessage.error, ...) which routes transient registration.update()
failures into the blocking install-error path; change this to emit a
non-blocking message instead (e.g., add a new enum/value like
BswupMessage.updateCheckFailed or use a non-fatal level such as
BswupMessage.warn/info) and call handle(...) with that new message name,
preserving the payload ({ reason: 'update', message: String((err && (err as
any).message) || err), reload }) so callers can still react but the default
progress handler will not hide the app or show the install-failed UI. Ensure to
update any switch/handler that consumes BswupMessage.* to treat the new
updateCheckFailed value as non-blocking.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: ae99fd6f-8550-4920-b5b5-7982f9b3572d
📒 Files selected for processing (7)
src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.jssrc/Bswup/Bit.Bswup/BswupProgress.razorsrc/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.tssrc/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.tssrc/Bswup/Bit.Bswup/Scripts/bit-bswup.tssrc/Bswup/Bit.Bswup/Styles/bit-bswup.progress.csssrc/Bswup/README.md
|
@coderabbitai re-review |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "=== bit-bswup.sw.ts ==="
fd "bit-bswup.sw.ts" --exec cat {}
echo ""
echo "=== bit-bswup.ts ==="
fd "bit-bswup.ts" -E "*.js" --exec cat {}
echo ""
echo "=== bit-bswup.progress.ts ==="
fd "bit-bswup.progress.ts" --exec cat {}
echo ""
echo "=== BswupProgress.razor ==="
fd "BswupProgress.razor" --exec cat {}
echo ""
echo "=== bit-bswup.progress.css ==="
fd "bit-bswup.progress.css" --exec cat {}
echo ""
echo "=== service-worker.js (demo) ==="
fd "service-worker.js" --search-path "Bit.Bswup.Demo" --exec cat {}
echo ""
echo "=== README.md (Bswup) ==="
cat src/Bswup/README.mdLength of output: 87174
🔴 Critical — Crash when
|
There was a problem hiding this comment.
Pull request overview
This PR improves Bit.Bswup’s update experience and robustness by adding user-visible install error reporting (with retry), introducing opt-in update polling (interval and/or tab visibility), improving multi-tab update consistency, and expanding Blazor script auto-detection to cover both Blazor Web App and standalone WebAssembly entry points.
Changes:
- Add update polling controls (
updateInterval,updateOnVisibility) and expose/extend JS API docs (BitBswup.checkForUpdate,skipWaiting,forceRefresh). - Improve service worker install reliability (manifest validation, retries/backoff, strict vs lax error tolerance, safer multi-tab activation/claim behavior).
- Enhance the progress UI with an error panel (message/details + retry) and corresponding styles.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Bswup/README.md | Documents new script attributes, error tolerance behavior, multi-tab behavior, security notes, and JS API. |
| src/Bswup/Bit.Bswup/Styles/bit-bswup.progress.css | Adds styling for the new install error panel and retry button behavior. |
| src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts | Adds idempotent initialization, update polling, better controllerchange handling, richer logging levels, and expanded Blazor script detection. |
| src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts | Adds manifest validation + error reporting, retry/backoff for asset fetches, strict/lax install semantics, safer cache update sequencing, and RegExp parsing improvements. |
| src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts | Adds UI handling for install errors (message/details/retry) and keeps assets list visibility in sync with runtime config changes. |
| src/Bswup/Bit.Bswup/BswupProgress.razor | Updates the progress component markup to include an error panel and changes initialization to run via a <script> block. |
| src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js | Removes demo precachedAssetsInclude config line. |
| src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js | Adjusts demo config to use errorTolerance = 'lax' with explanatory comment. |
|
@coderabbitai re-review |
|
✅ Actions performedFull review triggered. |
| <script> | ||
| BitBswupProgress.start(@(AutoReload ? "true" : "false"), @(ShowLogs ? "true" : "false"), @(ShowAssets ? "true" : "false"), '@(AppContainer)', @(HideApp ? "true" : "false"), @(AutoHide ? "true" : "false"), '@(Handler)'); | ||
| </script> |
| if (!MANIFEST_VALID) { | ||
| // The manifest is missing/malformed - sendError already notified the page. Don't | ||
| // build a cache from an empty/partial asset list (it would replace the previous good | ||
| // cache with a broken one). Let the lifecycle settle without caching anything. | ||
| diag('*** skipping install - invalid assetsManifest.'); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts (1)
333-362:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse
e.waitUntil()for async work in the service workermessagehandler.Returning a Promise from the
messagelistener doesn’t reliably keep the worker alive; useExtendableMessageEvent.waitUntil(...)forskipWaiting(),clients.claim(), cache deletion/filling, and the follow-uppostMessage.Suggested fix
-function handleMessage(e: MessageEvent<string>) { +function handleMessage(e: any) { diag('handleMessage:', e); if (e.data === 'SKIP_WAITING') { - return self.skipWaiting() - .then(() => self.clients.claim()) - .then(() => deleteOldCaches()) - .then(() => sendMessage('WAITING_SKIPPED')); + e.waitUntil( + self.skipWaiting() + .then(() => self.clients.claim()) + .then(() => deleteOldCaches()) + .then(() => sendMessage('WAITING_SKIPPED')) + ); + return; } if (e.data === 'CLAIM_CLIENTS') { - return self.clients.claim() - .then(() => deleteOldCaches()) - .then(() => e.source?.postMessage('CLIENTS_CLAIMED')); + e.waitUntil( + self.clients.claim() + .then(() => deleteOldCaches()) + .then(() => e.source?.postMessage('CLIENTS_CLAIMED')) + ); + return; } if (e.data === 'BLAZOR_STARTED') { - createAssetsCache(true); + e.waitUntil(createAssetsCache(true)); + return; } if (e.data === 'CLEAN_UP') { - deleteOldCaches(); // remove the old caches + e.waitUntil(deleteOldCaches()); // remove the old caches } }🤖 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/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts` around lines 333 - 362, The message handler currently returns Promises but must use e.waitUntil(...) to keep the service worker alive; wrap each async branch so that SKIP_WAITING uses e.waitUntil(self.skipWaiting().then(() => self.clients.claim()).then(() => deleteOldCaches()).then(() => sendMessage('WAITING_SKIPPED'))), CLAIM_CLIENTS uses e.waitUntil(self.clients.claim().then(() => deleteOldCaches()).then(() => e.source?.postMessage('CLIENTS_CLAIMED'))), BLAZOR_STARTED uses e.waitUntil(createAssetsCache(true)), and CLEAN_UP uses e.waitUntil(deleteOldCaches()), ensuring all calls to self.skipWaiting(), self.clients.claim(), deleteOldCaches(), createAssetsCache(true), sendMessage and e.source?.postMessage are inside the waitUntil chains.
🤖 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/Bswup/Bit.Bswup/BswupProgress.razor`:
- Around line 37-39: The inline script in BswupProgress.razor calls
BitBswupProgress.start(...) unguarded which throws if bit-bswup.progress.js
hasn't loaded; update the init to check for the global before invoking (e.g.,
use window.BitBswupProgress?.start(...) or an explicit if
(window.BitBswupProgress) call) passing the same parameters (AutoReload,
ShowLogs, ShowAssets, AppContainer, HideApp, AutoHide, Handler), or
alternatively move the startup into the component lifecycle (OnAfterRenderAsync)
and call JS interop to invoke BitBswupProgress.start to guarantee ordering.
- Line 38: The call to BitBswupProgress.start injects AppContainer and Handler
as raw single-quoted JS literals which can be broken by
quotes/backslashes/newlines; change those embeddings to JSON-encoded JS-safe
strings instead (e.g., replace the single-quoted '@(AppContainer)' and
'@(Handler)' with the output of
System.Text.Json.JsonSerializer.Serialize(AppContainer) and
JsonSerializer.Serialize(Handler) so the values are properly escaped and already
quoted), update the BswupProgress.razor to use JsonSerializer.Serialize for both
AppContainer and Handler and add the required using/import for System.Text.Json
if missing.
In `@src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts`:
- Around line 86-94: The cache bucket currently uses only CACHE_VERSION (derived
from self.cacheVersion or VERSION) which allows different manifests to share a
bucket; change the cache naming so the manifest VERSION is always part of the
bucket identity. Keep the existing CACHE_VERSION calculation (using
self.cacheVersion fallback to VERSION) but construct CACHE_NAME to include both
VERSION and CACHE_VERSION (e.g. `${CACHE_NAME_PREFIX} - ${VERSION} -
${CACHE_VERSION}`), so the manifest VERSION remains in the cache bucket while
cacheVersion can still be used to pin/force refresh.
- Around line 516-540: The current logic sets isIntegrity true whenever
hasIntegrity && fetchErr instanceof TypeError, which treats all TypeError fetch
rejections as SRI failures and prevents retries; change the detection in the
addCache flow so a bare TypeError does NOT automatically mark an integrity
failure: remove fetchErr instanceof TypeError from the isIntegrity predicate and
instead only classify as integrity when there is a clear integrity signal (e.g.,
the error message matches the integrity/digest regex) or after explicitly
recomputing and comparing the response bytes to asset.hash; keep transient
TypeError cases as retryable (use lastError/continue while attempt <
MAX_RETRIES), and only increment integrityFailureCount, set sendError.reason to
'integrity', and skip retries when you have a confirmed integrity mismatch (use
the integrity regex or the explicit digest comparison to decide).
In `@src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts`:
- Line 35: Guard runBswup against late script injection by checking
document.readyState before adding the listener: if the document is already
"interactive" or "complete" call runBswup() immediately, otherwise attach
window.addEventListener('DOMContentLoaded', runBswup) (refer to the runBswup
symbol). Also fix BitBswup.forceRefresh where cacheFilter.test(key) is used:
avoid inconsistent results when cacheFilter has the global (g) or sticky (y)
flags by resetting cacheFilter.lastIndex = 0 before each test or by creating a
non-global equivalent RegExp from cacheFilter (use cacheFilter.source with flags
stripped of g/y) and testing against that; reference the BitBswup.forceRefresh
method and the cacheFilter variable so the change is made exactly where repeated
RegExp.test calls occur.
- Around line 497-500: forceRefresh builds the shouldDelete predicate from
cacheFilter but when cacheFilter is a RegExp it calls cacheFilter.test()
repeatedly, which can misbehave for /.../g or /.../y because RegExp.lastIndex is
mutated; fix by updating the RegExp branch in the shouldDelete construction
(used in forceRefresh) to reset cacheFilter.lastIndex = 0 before calling
cacheFilter.test(key) so each key is tested from the start; reference the
shouldDelete variable and cacheFilter RegExp usage in forceRefresh when making
the change.
---
Outside diff comments:
In `@src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts`:
- Around line 333-362: The message handler currently returns Promises but must
use e.waitUntil(...) to keep the service worker alive; wrap each async branch so
that SKIP_WAITING uses e.waitUntil(self.skipWaiting().then(() =>
self.clients.claim()).then(() => deleteOldCaches()).then(() =>
sendMessage('WAITING_SKIPPED'))), CLAIM_CLIENTS uses
e.waitUntil(self.clients.claim().then(() => deleteOldCaches()).then(() =>
e.source?.postMessage('CLIENTS_CLAIMED'))), BLAZOR_STARTED uses
e.waitUntil(createAssetsCache(true)), and CLEAN_UP uses
e.waitUntil(deleteOldCaches()), ensuring all calls to self.skipWaiting(),
self.clients.claim(), deleteOldCaches(), createAssetsCache(true), sendMessage
and e.source?.postMessage are inside the waitUntil chains.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: d3c76fdc-5fec-4833-85fc-89e300ee0dd0
📒 Files selected for processing (8)
src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.jssrc/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.jssrc/Bswup/Bit.Bswup/BswupProgress.razorsrc/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.tssrc/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.tssrc/Bswup/Bit.Bswup/Scripts/bit-bswup.tssrc/Bswup/Bit.Bswup/Styles/bit-bswup.progress.csssrc/Bswup/README.md
💤 Files with no reviewable changes (1)
- src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js
| <script> | ||
| BitBswupProgress.start(@(AutoReload ? "true" : "false"), @(ShowLogs ? "true" : "false"), @(ShowAssets ? "true" : "false"), '@(AppContainer)', @(HideApp ? "true" : "false"), @(AutoHide ? "true" : "false"), '@(Handler)'); | ||
| </script> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether any host page renders the component before loading bit-bswup.progress.js.
rg -n --iglob '*.{razor,cshtml,html,md}' '<(Bit)?BswupProgress|bit-bswup\.progress(\.min)?\.js|BitBswupProgress\.start' . -C2Repository: bitfoundation/bitplatform
Length of output: 14029
Guard against missing bit-bswup.progress.js when initializing BitBswupProgress.
src/Bswup/Bit.Bswup/BswupProgress.razor calls BitBswupProgress.start(...) in an inline <script> during markup parse; if _content/Bit.Bswup/bit-bswup.progress.js hasn’t loaded yet, BitBswupProgress will be undefined and the progress UI won’t wire up. First-party usage does load the script tag before rendering the component (e.g., src/Bswup/FullDemo/Server/Pages/_Layout.cshtml and src/Websites/Platform/src/Bit.Websites.Platform.Server/Components/App.razor), but the component itself has no runtime guard—add a check (e.g., window.BitBswupProgress?.start(...)) or move the init to ensure ordering isn’t a consumer footgun.
🤖 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/Bswup/Bit.Bswup/BswupProgress.razor` around lines 37 - 39, The inline
script in BswupProgress.razor calls BitBswupProgress.start(...) unguarded which
throws if bit-bswup.progress.js hasn't loaded; update the init to check for the
global before invoking (e.g., use window.BitBswupProgress?.start(...) or an
explicit if (window.BitBswupProgress) call) passing the same parameters
(AutoReload, ShowLogs, ShowAssets, AppContainer, HideApp, AutoHide, Handler), or
alternatively move the startup into the component lifecycle (OnAfterRenderAsync)
and call JS interop to invoke BitBswupProgress.start to guarantee ordering.
| <img style="display: none" src="" | ||
| onerror="BitBswupProgress.start(@(AutoReload ? "true" : "false"), @(ShowLogs ? "true" : "false"), @(ShowAssets ? "true" : "false"), '@(AppContainer)', @(HideApp ? "true" : "false"), @(AutoHide ? "true" : "false"), '@(Handler)')"> | ||
| <script> | ||
| BitBswupProgress.start(@(AutoReload ? "true" : "false"), @(ShowLogs ? "true" : "false"), @(ShowAssets ? "true" : "false"), '@(AppContainer)', @(HideApp ? "true" : "false"), @(AutoHide ? "true" : "false"), '@(Handler)'); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In a Razor component, is HTML encoding sufficient when inserting @(value)into a JavaScript single-quoted string inside a<script> tag, or should the value be JSON/JavaScript-string encoded instead?
💡 Result:
No, standard HTML encoding is not sufficient when inserting a value into a JavaScript string literal inside a script tag [1][2]. Razor's automatic encoding uses HTML attribute encoding rules for any expression preceded by the @ symbol [1][3]. This is designed to prevent Cross-Site Scripting (XSS) in HTML contexts (such as content within an element or an HTML attribute), but it is not intended for and does not secure JavaScript contexts [1][2]. If you insert an unencoded or HTML-encoded value directly into a JavaScript string, you face a significant security risk. For example, if a user provides an input like '; alert(1); //, and you place it inside a JavaScript string literal, an attacker can break out of the string context and execute arbitrary code in the browser [1][2]. The recommended approach to pass data from a Razor component to JavaScript is to avoid embedding the data directly in the script block [1][2]. Instead, you should: 1. Use HTML5 data attributes: Store the data in a non-visible element's data attribute (e.g., <div id="data" data-value="@value"></div>) and retrieve it using JavaScript (e.g., document.getElementById('data').dataset.value) [1][2]. This approach leverages the browser's built-in, secure parsing of HTML attributes. 2. Use JSON serialization if you must embed data: If embedding in a script tag is strictly necessary, you must use a proper JSON serializer (like System.Text.Json.JsonSerializer.Serialize) to encode the value as a valid JSON string, and then ensure that the output is handled safely [4][5]. However, using data attributes remains the standard and most secure best practice in ASP.NET Core applications [1][2]. Avoid using Html.Raw to inject data directly into JavaScript, as it bypasses all encoding and directly introduces the risk of XSS [1][3][6].
Citations:
- 1: https://learn.microsoft.com/en-us/aspnet/core/security/cross-site-scripting?view=aspnetcore-8.0
- 2: https://learn.microsoft.com/en-us/aspnet/core/security/cross-site-scripting?view=aspnetcore-10.0
- 3: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-10.0
- 4: https://stackoverflow.com/questions/8300605/how-to-encode-embedded-javascript-in-razor-view-in-asp-net-mvc-3
- 5: https://stackoverflow.com/questions/14731421/output-single-quotes-in-razor-generated-javascript-string
- 6: https://stackoverflow.com/questions/15950599/correctly-using-quote-marks-in-mvc-view-razor
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect the target Razor component around the flagged line
FILE="src/Bswup/Bit.Bswup/BswupProgress.razor"
if [ -f "$FILE" ]; then
echo "---- $FILE (around line 20-80) ----"
nl -ba "$FILE" | sed -n '1,120p'
else
echo "Missing file: $FILE" >&2
fi
# 2) Find where BitBswupProgress.start is defined
echo "---- Find BitBswupProgress.start definitions/usages ----"
rg -n --hidden --no-ignore -S "BitBswupProgress\.start" .
# 3) For any definition files found, print a small relevant window around "start("
# (This is to see whether it evals/execs the passed strings)
rg -n --hidden --no-ignore -S "start\s*\(" . | head -n 50Repository: bitfoundation/bitplatform
Length of output: 200
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/Bswup/Bit.Bswup/BswupProgress.razor"
echo "---- $FILE (around line 20-80) ----"
if [ -f "$FILE" ]; then
awk 'NR>=20 && NR<=90 { printf "%6d %s\n", NR, $0 }' "$FILE"
else
echo "Missing file: $FILE" >&2
fi
echo "---- Find BitBswupProgress.start definitions/usages ----"
rg -n --hidden --no-ignore -S "BitBswupProgress\.start" .
echo "---- Find 'start(' call sites (limited) ----"
rg -n --hidden --no-ignore -S "start\s*\(" src/Bswup/Bit.Bswup . | head -n 50Repository: bitfoundation/bitplatform
Length of output: 10141
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/Bswup/Bit.Bswup/BswupProgress.razor"
echo "---- File line count ----"
wc -l "$FILE" || true
echo "---- BswupProgress.razor (top 1-140) ----"
awk 'NR>=1 && NR<=140 { printf "%6d %s\n", NR, $0 }' "$FILE"
echo "---- Search for BitBswupProgress in repo (JavaScript/CS/CSHTML) ----"
rg -n --hidden --no-ignore -S "BitBswupProgress" .
echo "---- Search for start implementation (JS) ----"
# try to locate JS file that defines BitBswupProgress.start
rg -n --hidden --no-ignore -S "BitBswupProgress\s*\.\s*start" .
# also look for BitBswupProgress object literal/assignment patterns
rg -n --hidden --no-ignore -S "(BitBswupProgress\s*=|var\s+BitBswupProgress|const\s+BitBswupProgress|function\s+BitBswupProgress)" .Repository: bitfoundation/bitplatform
Length of output: 3829
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts"
echo "---- file line count ----"
wc -l "$FILE" || true
echo "---- bit-bswup.progress.ts (top 1-200) ----"
awk 'NR>=1 && NR<=220 { printf "%6d %s\n", NR, $0 }' "$FILE"
echo "---- Search for usage of appContainer/handler parameters ----"
rg -n --hidden --no-ignore -S "appContainer|handler|AppContainer|Handler" "$FILE"
echo "---- Search for eval/new Function/innerHTML/document.write ----"
rg -n --hidden --no-ignore -S "eval\s*\(|new Function|innerHTML|document\.write" "$FILE"Repository: bitfoundation/bitplatform
Length of output: 12179
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/index.html"
echo "---- file line count ----"
wc -l "$FILE" || true
echo "---- index.html around BitBswupProgress.start (190-260) ----"
awk 'NR>=180 && NR<=260 { printf "%6d %s\n", NR, $0 }' "$FILE"
echo "---- Search in index.html for appContainer/handler definitions ----"
rg -n --hidden --no-ignore -S "appContainer|handler|BitBswupProgress\.start" "$FILE"Repository: bitfoundation/bitplatform
Length of output: 3236
🌐 Web query:
System.Text.Json JsonSerializer.Serialize default string escaping <script> </script> JavaScriptEncoder.Default unsafe relaxed json escaping
💡 Result:
In System.Text.Json, the default string escaping behavior is designed to prioritize security and defense-in-depth [1][2]. When you use JsonSerializer.Serialize with default settings, the serializer utilizes the default JavaScriptEncoder [3]. This encoder automatically escapes HTML-sensitive characters—such as <, >, &, ', and "—within the ASCII range, as well as all non-ASCII characters [3][1][2]. Consequently, strings containing <script> and </script> tags will have their brackets escaped (e.g., \u003Cscript\u003E), which prevents these tags from being interpreted as HTML when the JSON is embedded directly into an HTML page or a script element [1][2][4]. To modify this behavior, you can customize the Encoder property within JsonSerializerOptions [1][5]. JavaScriptEncoder.UnsafeRelaxedJsonEscaping is a built-in, more permissive encoder [1][2]. Unlike the default encoder, it does not escape HTML-sensitive characters like <, >, or &, and it allows most Unicode characters to pass through unescaped [1][6][2]. Because this encoder lacks the default defense-in-depth protections, its name includes "Unsafe" as a clear warning [2][7]. Microsoft explicitly advises that you must never allow the output of this encoder to be emitted into an HTML page or a <script> element, as doing so introduces a significant risk of cross-site scripting (XSS) attacks [1][2]. It should only be used when the resulting payload is strictly interpreted as UTF-8 encoded JSON (e.g., when sent as a response with the header Content-Type: application/json; charset=utf-8) [1][6][7].
Citations:
- 1: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/character-encoding
- 2: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/docs/ThreatModel.md
- 3: https://github.com/dotnet/docs/blob/main/docs/standard/serialization/system-text-json/how-to.md
- 4: https://www.codestudy.net/blog/dotnet-core-system-text-json-unescape-unicode-string/
- 5: https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions.encoder?view=net-10.0
- 6: https://stackoverflow.com/questions/10756232/json-net-serializeobject-escape-values-to-prevent-xss
- 7: https://stackoverflow.com/questions/58003293/dotnet-core-system-text-json-unescape-unicode-string
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- Search for MarkupString usage in Razor components ----"
rg -n --hidden --no-ignore -S "MarkupString" src | head -n 50
echo "---- Search for System.Text.Json usage in Razor components ----"
rg -n --hidden --no-ignore -S "System\.Text\.Json|JsonSerializer\.Serialize" src --glob='*.razor' | head -n 50
echo "---- Search for JsonSerializer.Serialize + MarkupString combo ----"
rg -n --hidden --no-ignore -S "JsonSerializer\.Serialize.*MarkupString|MarkupString.*JsonSerializer\.Serialize" src --glob='*.razor' | head -n 50Repository: bitfoundation/bitplatform
Length of output: 6016
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- Search for JsonSerializer.Serialize usage (anywhere) ----"
rg -n --hidden --no-ignore -S "JsonSerializer\.Serialize" .
echo "---- Search for System.Text.Json imports in Razor files ----"
rg -n --hidden --no-ignore -S "System\.Text\.Json|JsonSerializer" --glob='*.razor' .
echo "---- Search for `@using` System.Text.Json in .razor ----"
rg -n --hidden --no-ignore -S "`@using`\s+System\.Text\.Json" --glob='*.razor' .Repository: bitfoundation/bitplatform
Length of output: 1972
JS-encode AppContainer and Handler before embedding them into the <script> call
src/Bswup/Bit.Bswup/BswupProgress.razor line 38 injects [Parameter] values AppContainer and Handler directly into JavaScript single-quoted literals inside a <script> tag. If either contains ', \, newlines, or </script>, it can break out of the string literal and inject JS.
Proposed fix
+@using System.Text.Json
+
<script>
- BitBswupProgress.start(@(AutoReload ? "true" : "false"), @(ShowLogs ? "true" : "false"), @(ShowAssets ? "true" : "false"), '@(AppContainer)', @(HideApp ? "true" : "false"), @(AutoHide ? "true" : "false"), '@(Handler)');
+ BitBswupProgress.start(
+ @(AutoReload ? "true" : "false"),
+ @(ShowLogs ? "true" : "false"),
+ @(ShowAssets ? "true" : "false"),
+ @((MarkupString)JsonSerializer.Serialize(AppContainer)),
+ @(HideApp ? "true" : "false"),
+ @(AutoHide ? "true" : "false"),
+ @((MarkupString)JsonSerializer.Serialize(Handler))
+ );
</script>🤖 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/Bswup/Bit.Bswup/BswupProgress.razor` at line 38, The call to
BitBswupProgress.start injects AppContainer and Handler as raw single-quoted JS
literals which can be broken by quotes/backslashes/newlines; change those
embeddings to JSON-encoded JS-safe strings instead (e.g., replace the
single-quoted '@(AppContainer)' and '@(Handler)' with the output of
System.Text.Json.JsonSerializer.Serialize(AppContainer) and
JsonSerializer.Serialize(Handler) so the values are properly escaped and already
quoted), update the BswupProgress.razor to use JsonSerializer.Serialize for both
AppContainer and Handler and add the required using/import for System.Text.Json
if missing.
| // Cache identity normally tracks Blazor's manifest version (assetsManifest.version), a | ||
| // hash over the published assets. cacheVersion lets an app override the value used in the | ||
| // cache name: pin a stable string across noisy dev rebuilds (so perturbed asset hashes | ||
| // don't needlessly evict the whole cache), or bump it to force a refresh when a meaningful | ||
| // change lives outside Blazor's asset manifest. Only the cache *bucket name* is affected; | ||
| // the per-asset `?v=` cache-buster and SRI hashes still derive from VERSION, so integrity | ||
| // is unchanged. Falls back to the manifest version when unset or not a non-empty string. | ||
| const CACHE_VERSION = (typeof self.cacheVersion === 'string' && self.cacheVersion) || VERSION; | ||
| const CACHE_NAME = `${CACHE_NAME_PREFIX} - ${CACHE_VERSION}`; |
There was a problem hiding this comment.
Keep VERSION in the cache bucket identity.
Using cacheVersion as the entire cache name lets different manifests share one bucket. The waiting worker then updates the same cache the active worker is serving from, so old tabs can observe partially updated index.html / assets before activation.
Suggested fix
-const CACHE_NAME = `${CACHE_NAME_PREFIX} - ${CACHE_VERSION}`;
+const CACHE_NAME = `${CACHE_NAME_PREFIX} - ${CACHE_VERSION} - ${VERSION}`;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Cache identity normally tracks Blazor's manifest version (assetsManifest.version), a | |
| // hash over the published assets. cacheVersion lets an app override the value used in the | |
| // cache name: pin a stable string across noisy dev rebuilds (so perturbed asset hashes | |
| // don't needlessly evict the whole cache), or bump it to force a refresh when a meaningful | |
| // change lives outside Blazor's asset manifest. Only the cache *bucket name* is affected; | |
| // the per-asset `?v=` cache-buster and SRI hashes still derive from VERSION, so integrity | |
| // is unchanged. Falls back to the manifest version when unset or not a non-empty string. | |
| const CACHE_VERSION = (typeof self.cacheVersion === 'string' && self.cacheVersion) || VERSION; | |
| const CACHE_NAME = `${CACHE_NAME_PREFIX} - ${CACHE_VERSION}`; | |
| // Cache identity normally tracks Blazor's manifest version (assetsManifest.version), a | |
| // hash over the published assets. cacheVersion lets an app override the value used in the | |
| // cache name: pin a stable string across noisy dev rebuilds (so perturbed asset hashes | |
| // don't needlessly evict the whole cache), or bump it to force a refresh when a meaningful | |
| // change lives outside Blazor's asset manifest. Only the cache *bucket name* is affected; | |
| // the per-asset `?v=` cache-buster and SRI hashes still derive from VERSION, so integrity | |
| // is unchanged. Falls back to the manifest version when unset or not a non-empty string. | |
| const CACHE_VERSION = (typeof self.cacheVersion === 'string' && self.cacheVersion) || VERSION; | |
| const CACHE_NAME = `${CACHE_NAME_PREFIX} - ${CACHE_VERSION} - ${VERSION}`; |
🤖 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/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts` around lines 86 - 94, The cache
bucket currently uses only CACHE_VERSION (derived from self.cacheVersion or
VERSION) which allows different manifests to share a bucket; change the cache
naming so the manifest VERSION is always part of the bucket identity. Keep the
existing CACHE_VERSION calculation (using self.cacheVersion fallback to VERSION)
but construct CACHE_NAME to include both VERSION and CACHE_VERSION (e.g.
`${CACHE_NAME_PREFIX} - ${VERSION} - ${CACHE_VERSION}`), so the manifest VERSION
remains in the cache bucket while cacheVersion can still be used to pin/force
refresh.
| const isIntegrity = | ||
| hasIntegrity && | ||
| (fetchErr instanceof TypeError || | ||
| /integrity|digest|EPRPROTO|ERR_FAILED/i.test(String(fetchErr && (fetchErr as any).message || fetchErr))); | ||
|
|
||
| // Integrity failures are deterministic: re-fetching identical bytes fails the | ||
| // same way, so never retry them. Genuine network errors are transient and | ||
| // worth another attempt while retries remain. | ||
| if (!isIntegrity && attempt < MAX_RETRIES) { | ||
| lastError = fetchErr; | ||
| diag('*** addCache - fetch rejected (will retry):', fetchErr, asset.url); | ||
| continue; | ||
| } | ||
|
|
||
| if (isIntegrity) integrityFailureCount++; | ||
| diag('*** addCache - fetch rejected:', fetchErr, 'integrity?', isIntegrity); | ||
| sendError({ | ||
| reason: isIntegrity ? 'integrity' : 'fetch', | ||
| message: isIntegrity | ||
| ? `Subresource Integrity check failed for ${asset.url}. The bytes served do not match the SHA hash recorded in service-worker-assets.js / blazor.boot.json. This is the classic Blazor "Failed to find a valid digest" failure and usually means a CDN, reverse proxy, or compression layer is rewriting the response after publish.` | ||
| : 'Asset fetch rejected' + (attempt > 0 ? ` after ${attempt + 1} attempts` : '') + ': ' + (fetchErr && (fetchErr as any).message || String(fetchErr)), | ||
| url: asset.url, | ||
| hash: asset.hash, | ||
| integrity: hasIntegrity, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For Fetch API requests with RequestInit.integrity, do both transient network failures and Subresource Integrity failures reject with TypeError? Is there a reliable signal a service worker can use to distinguish them?
💡 Result:
Yes: for Fetch requests using RequestInit.integrity, both transient network failures and subresource integrity (SRI) digest mismatches end up rejecting the fetch promise with a TypeError (the browser reports the integrity failure as a network error). In MDN’s RequestInit.integrity documentation, a mismatch “will reject the fetch request with a network error” [1]. Separately, MDN states that fetch “promise[s] reject with a TypeError when a network error is encountered” [2]. Taken together, SRI mismatches and true network errors both manifest as TypeError rejections from fetch. Is there a reliable signal for a service worker to distinguish them? No—there is no reliable, standardized signal exposed to a service worker to tell “integrity mismatch” apart from “network error” when using fetch(Request) with integrity. The main observable contract is the rejection itself (TypeError), and both categories map to “network error” at the fetch promise level [1][2]. Service-worker code therefore cannot depend on error type/messages/status to discriminate the two in a portable way. Practical implication for service workers: treat any fetch rejection involving integrity as non-recoverable (or recover via an independent strategy), but do not assume you can identify whether it was an integrity mismatch vs connectivity/CORS/etc. via the thrown TypeError alone. One known special-case behavior is that using integrity in no-cors scenarios can produce a TypeError even when the underlying issue is the request/response mode, underscoring that you can’t count on the thrown TypeError to cleanly classify the root cause [3].
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
- 2: https://devdoc.net/web/developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/fetch.html
- 3: TypeError on Request.integrity with no-cors mode is a foot gun whatwg/fetch#583
Don’t classify all TypeError on integrity requests as SRI failures
MDN documents that both (1) SRI digest mismatches and (2) real transient network failures can surface to fetch as the same rejection: a “network error” TypeError. Your logic treats any TypeError as isIntegrity when hasIntegrity is true, which prevents retries for transient outages and reports them as integrity problems.
- Adjust
isIntegritydetection sofetchErr instanceof TypeErrordoesn’t automatically imply digest/SRI failure (e.g., retry onTypeError, or only mark integrity after stronger signals / by recomputing and comparing the digest with the expected hash).
🤖 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/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts` around lines 516 - 540, The
current logic sets isIntegrity true whenever hasIntegrity && fetchErr instanceof
TypeError, which treats all TypeError fetch rejections as SRI failures and
prevents retries; change the detection in the addCache flow so a bare TypeError
does NOT automatically mark an integrity failure: remove fetchErr instanceof
TypeError from the isIntegrity predicate and instead only classify as integrity
when there is a clear integrity signal (e.g., the error message matches the
integrity/digest regex) or after explicitly recomputing and comparing the
response bytes to asset.hash; keep transient TypeError cases as retryable (use
lastError/continue while attempt < MAX_RETRIES), and only increment
integrityFailureCount, set sendError.reason to 'integrity', and skip retries
when you have a confirmed integrity mismatch (use the integrity regex or the
explicit digest comparison to decide).
|
|
||
| const bitBswupScript = document.currentScript; | ||
|
|
||
| window.addEventListener('DOMContentLoaded', runBswup); // important event! |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
If a script adds window.addEventListener('DOMContentLoaded', callback)after theDOMContentLoaded event has already fired, will that callback ever run?
💡 Result:
No—if the DOMContentLoaded event has already fired, adding a listener with window.addEventListener('DOMContentLoaded', callback) after that point will not cause callback to run, because the DOMContentLoaded event will not be re-delivered to new listeners once it has already occurred. [1][2] How to handle this: check document.readyState before adding the listener; if it’s already past loading (e.g., "interactive" or "complete"), run your logic immediately instead of relying on the event. [3][2]
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event
- 2: https://stackoverflow.com/questions/39993676/code-inside-domcontentloaded-event-not-working
- 3: https://stackoverflow.com/questions/9457891/how-to-detect-if-domcontentloaded-was-fired
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts"
echo "== file =="
wc -l "$FILE"
echo
echo "== around line 35 =="
nl -ba "$FILE" | sed -n '20,60p'
echo
echo "== around forceRefresh (assumed ~497-500) =="
nl -ba "$FILE" | sed -n '450,560p'Repository: bitfoundation/bitplatform
Length of output: 209
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts"
echo "== around line 35 =="
awk 'NR>=25 && NR<=45 {printf "%4d: %s\n", NR, $0}' "$FILE"
echo
echo "== around line 470-530 (forceRefresh/cacheFilter) =="
awk 'NR>=470 && NR<=530 {printf "%4d: %s\n", NR, $0}' "$FILE"Repository: bitfoundation/bitplatform
Length of output: 4128
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts"
echo "== occurrences of forceRefresh or cacheFilter usage =="
rg -n "forceRefresh|cacheFilter|lastIndex|test\\(key\\)" "$FILE" || true
echo
echo "== occurrences of SKIP_WAITING / updateCheckFailed for context =="
rg -n "updateCheckFailed|runBswup\\(|DOMContentLoaded" "$FILE" || trueRepository: bitfoundation/bitplatform
Length of output: 1170
Guard runBswup() against late script injection (DOMContentLoaded might have already fired)
window.addEventListener('DOMContentLoaded', runBswup)at line 35 won’t fire if this script is injected afterDOMContentLoadedhas already occurred, leaving the runtime inert.BitBswup.forceRefreshuses(key) => cacheFilter.test(key)(lines 498-500); if the providedRegExphasg/y, repeatedtest()calls can be inconsistent due tolastIndex.
Proposed fix
- window.addEventListener('DOMContentLoaded', runBswup); // important event!
+ if document.readyState === 'loading') {
+ window.addEventListener('DOMContentLoaded', runBswup); // important event!
+ } else {
+ runBswup();
+ }🤖 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/Bswup/Bit.Bswup/Scripts/bit-bswup.ts` at line 35, Guard runBswup against
late script injection by checking document.readyState before adding the
listener: if the document is already "interactive" or "complete" call runBswup()
immediately, otherwise attach window.addEventListener('DOMContentLoaded',
runBswup) (refer to the runBswup symbol). Also fix BitBswup.forceRefresh where
cacheFilter.test(key) is used: avoid inconsistent results when cacheFilter has
the global (g) or sticky (y) flags by resetting cacheFilter.lastIndex = 0 before
each test or by creating a non-global equivalent RegExp from cacheFilter (use
cacheFilter.source with flags stripped of g/y) and testing against that;
reference the BitBswup.forceRefresh method and the cacheFilter variable so the
change is made exactly where repeated RegExp.test calls occur.
| const shouldDelete = | ||
| typeof cacheFilter === 'function' ? cacheFilter : | ||
| cacheFilter instanceof RegExp ? (key: string) => cacheFilter.test(key) : | ||
| typeof cacheFilter === 'string' ? (key: string) => key.startsWith(cacheFilter) : |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does JavaScript RegExp.prototype.test()mutatelastIndexfor regexes with thegoryflag, making repeatedtest() calls stateful?
💡 Result:
Yes, JavaScript RegExp.prototype.test mutates the lastIndex property when the regular expression instance has the global (g) or sticky (y) flags set, which makes repeated calls stateful [1][2][3]. When a regex includes the g or y flag, test uses the current value of the lastIndex property to determine where to begin its search [1][4][5]. If a match is found, lastIndex is updated to the position immediately following the end of that match [1][4][5]. This behavior allows subsequent calls to test to continue searching from where the previous match left off [1][2]. If no match is found, lastIndex is reset to 0 [1][4][5]. This stateful behavior is a common source of bugs if not intended, as the regex instance remembers its position even when testing different strings [1][6][7]. To avoid this, you can manually reset lastIndex to 0 after a match or create a new regex instance for each test [4][7]. Regexes without these flags are not stateful and do not use or update lastIndex [1][3].
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test
- 2: https://github.com/mdn/content/blob/main/files/en-us/web/javascript/reference/global_objects/regexp/test/index.md
- 3: https://2ality.com/2020/01/regexp-lastindex.html
- 4: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastIndex
- 5: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky
- 6: https://forum.freecodecamp.org/t/mutations-using-regex-passing-all-tests-but-one/302801
- 7: https://banach.net.pl/posts/2023/when-javascript-regex-can-surprise-you-a-silly-bug-story/
Reset RegExp.lastIndex when reusing cacheFilter.test() across cache keys (src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts, lines 497-500)
forceRefresh() builds shouldDelete using the same cacheFilter RegExp instance for every entry in caches.keys().filter(...). For /.../g or /.../y, RegExp.prototype.test() mutates lastIndex, so later keys can be evaluated with stale state and be skipped.
Proposed fix
const shouldDelete =
typeof cacheFilter === 'function' ? cacheFilter :
cacheFilter instanceof RegExp ? (key: string) => {
cacheFilter.lastIndex = 0;
return cacheFilter.test(key);
} :
typeof cacheFilter === 'string' ? (key: string) => key.startsWith(cacheFilter) :
() => true;🤖 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/Bswup/Bit.Bswup/Scripts/bit-bswup.ts` around lines 497 - 500,
forceRefresh builds the shouldDelete predicate from cacheFilter but when
cacheFilter is a RegExp it calls cacheFilter.test() repeatedly, which can
misbehave for /.../g or /.../y because RegExp.lastIndex is mutated; fix by
updating the RegExp branch in the shouldDelete construction (used in
forceRefresh) to reset cacheFilter.lastIndex = 0 before calling
cacheFilter.test(key) so each key is tested from the start; reference the
shouldDelete variable and cacheFilter RegExp usage in forceRefresh when making
the change.
closes #12408
Summary by CodeRabbit
New Features
Bug Fixes
Documentation