Conversation
Embed the official Beacon loader snippet as a nonce-carrying inline script (bodyClose) in nuxt.config.ts and init it with our Beacon ID. Allow the required HelpScout origins in the CSP (connect/style/font/ frame/media-src) per their documented requirements; object-src stays 'none'. Add a regression test locking the connect-src entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughChangesAdds a configurable in-app HelpScout support panel with article search, conversation submission, attachments, diagnostics, server-side API integration, and Beacon/CSP support. Also introduces early redacted console capture and documents configuration, scope, and compliance considerations. ChangesHelpScout Support Integration
Estimated code review effort: 4 (Complex) | ~60 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
nuxt.config.ts (1)
7-11: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueBeacon ID is hardcoded rather than configured.
The PR objective describes initialization "with the configured Beacon ID", but
29adfc12-...is inlined directly. It's a public workspace identifier (not a secret), so this is not a security issue, but sourcing it from runtime config would ease per-environment reuse. Since the script is a staticinnerHTMLstring, this would require building the snippet fromuseRuntimeConfig().publicat render time — optional.🤖 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 `@nuxt.config.ts` around lines 7 - 11, The HelpScout Beacon init snippet in helpScoutBeaconScript is hardcoding the Beacon ID instead of using configured runtime values. Update the Nuxt config logic to read the Beacon ID from useRuntimeConfig().public at render time and interpolate it into the script string before it is injected, keeping the existing Beacon loader behavior intact. Use the helpScoutBeaconScript constant as the main place to make this change.
🤖 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.
Nitpick comments:
In `@nuxt.config.ts`:
- Around line 7-11: The HelpScout Beacon init snippet in helpScoutBeaconScript
is hardcoding the Beacon ID instead of using configured runtime values. Update
the Nuxt config logic to read the Beacon ID from useRuntimeConfig().public at
render time and interpolate it into the script string before it is injected,
keeping the existing Beacon loader behavior intact. Use the
helpScoutBeaconScript constant as the main place to make this change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: euler-xyz/coderabbit/.coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 831da435-c875-4345-bc22-bd90ca93f2f0
📒 Files selected for processing (3)
nuxt.config.tsserver/plugins/csp.tstests/server/security.test.ts
The Beacon container mounts asynchronously after window load, so a route watcher in app.vue toggles a beacon-hidden root class instead of touching the element, and main.scss hides the container under it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🚅 Deployed to the euler-lite-pr-705 environment in euler-lite(dev,PR previews)
|
Watch the wagmi address and push it via Beacon('session-data') so
support agents see the wallet on every conversation without the user
typing it. The Beacon embed shim queues calls made before the script
loads, so the immediate watcher run is safe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Beacon has no read-only form fields, so surface the attachment to the user via the responseTime sublabel in the form header — visible but not editable. Updated dynamically alongside session-data whenever the connected address changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A ring buffer (installed at boot by 00.console-capture.client.ts) keeps the last 100 console entries with query-string values redacted so URL-borne API keys never reach a ticket. When the user opens the Beacon, the recent output and an app-state snapshot (url, route, chain, wallet, theme, viewport, UA) are attached via session-data, merged with the wallet entry so calls don't clobber each other. The form-header notice now mentions the diagnostics attachment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LeonardEulerXYZ
left a comment
There was a problem hiding this comment.
Leonard review — changes requested
Reviewed head a75a06f71980e72af43bacbe271346a03b922b04.
Walkthrough and verdict
This adds the HelpScout loader/CSP surface, hides the launcher on onboarding, enriches support conversations with wallet/app state, and installs an app-wide console ring buffer. The Beacon itself renders and opens correctly on the current Railway preview, but the diagnostic capture introduces a blocking disclosure path: arbitrary console values can be forwarded to HelpScout with only query-string redaction.
Blocking finding
utils/console-capture.ts:23-42— the global capture serializes everylog/info/warn/error/debugargument, whileQUERY_VALUE_REmasks only URL query values. Common path-based RPC credentials, bearer tokens, and structured fields such as{ token, email }survive unchanged and are attached on Beacon open fromapp.vue:159-171. A local regression probe against this exact implementation retained all four examples. See the inline comment for the required boundary.
Scalability / maintainability hygiene
The support integration is app-global, so its privacy boundary must cover every route and dependency logger rather than one known URL shape. The patch also creates a second, substantially weaker redaction implementation beside utils/client-observability.ts:65-74. Prefer one shared, tested sanitizer—or, better, an allowlisted diagnostic payload—rather than growing parallel regex sets. The hard-coded Beacon ID noted by CodeRabbit is a valid deployment-hygiene suggestion, but non-blocking here; CodeRabbit's review was attached to an earlier commit and did not cover the later console-capture surface.
Validation
git diff --check— passed- Focused Vitest:
tests/server/security.test.ts,tests/utils/console-capture.test.ts, plus a removed local-only credential-retention probe — 28/28 passed npm run typecheck— passednpm run build— passed (existing bundle/sourcemap warnings only)- Headed Chromium/Xvfb against the Railway preview:
- desktop 1440×1000: real Explore page, Beacon opens, diagnostics notice visible
- mobile 390×844: Beacon opens and form fits; it intentionally overlays the app/menu while open
- no page errors; one non-blocking third-party 403 console resource error
Smoke coverage: browser visual smoke + mobile smoke; disconnected only. Wallet connection/signing was not exercised.
Screenshot evidence
Desktop — HelpScout form open on Explore (1440×1000):
Mobile — HelpScout form open on Explore (390×844):
| const text = args | ||
| .map(stringifyArg) | ||
| .join(' ') | ||
| .replace(QUERY_VALUE_RE, '$1[redacted]') |
There was a problem hiding this comment.
🔴 Blocking — global console capture can forward credentials and PII. This regex only redacts query-string values, but the plugin serializes every argument from every console level and app.vue later attaches the buffer to HelpScout. A local probe retained a path-based RPC key (.../v2/<key>), a bearer token, and structured { token, email } values unchanged. Existing app/dependency errors can contain request URLs, bodies, signatures, or user data, so truncation does not make this boundary safe. Please replace arbitrary console collection with an allowlisted diagnostic payload, or centralize a structured recursive sanitizer that covers URLs, auth headers/tokens, secret-like keys, calldata/signatures, and nested error shapes; add realistic regression cases. utils/client-observability.ts already contains a safer allowlisted/redaction pattern that should not be duplicated more weakly here.
Implements the Claude Design "Support Window" spec: a support panel built from the app's own components, tokens and theme, opened from Support in the header Resources menu. HelpScout stays the backend (Docs API for search, Mailbox API v2 for the ticket) but every call is server-side via /api/internal/support/*, so nothing third-party runs on the page and no CSP entries are needed. Ships browse + article + compose + confirmation. Reading past conversations is deliberately left out: HelpScout keys conversations by customer email, so that view needs an email-authorized read endpoint which would let anyone knowing an address read that user's history. Off by default (configEnableSupportPanel) because the HELPSCOUT_* server credentials do not exist in any environment yet — the Beacon loader and its CSP entries are untouched so today's support channel keeps working. When the flag is on, the Beacon launcher is hidden so there is a single support surface. Fixes found while adapting the reference implementation: composables resolved during setup instead of inside the send handler (would throw "nuxt instance unavailable"), UiAlert given its required title prop instead of slot content (error was invisible), and h-112 replaced with an arbitrary value since the spacing scale stops at 100. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
docs/support-panel.md (1)
19-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd language identifiers to fenced code blocks (markdownlint MD040).
📝 Proposed fix
-``` +```env HELPSCOUT_DOCS_API_KEY=… # Docs API key (Basic auth, key as username) HELPSCOUT_DOCS_COLLECTION_ID=… # optional: scope search to one collection HELPSCOUT_APP_ID=… # Mailbox API v2 OAuth2 client id HELPSCOUT_APP_SECRET=… HELPSCOUT_MAILBOX_ID=… # mailbox new conversations land in```diff -``` +```text composables/useSupportPanel.ts shared state + data access components/support/SupportPanel.vue the panel itself components/support/SupportPanelHost.vue teleported host, outside-click / Esc server/utils/helpscout.ts Docs + Mailbox clients, token cache server/api/internal/support/docs.get.ts article search / read server/api/internal/support/conversations.post.ts create the conversation</details> Also applies to: 33-40 <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.In
@docs/support-panel.mdaround lines 19 - 25, Update the fenced code blocks in
the support panel documentation to include language identifiers: mark the
environment-variable block as env and the file-list block around the referenced
support-panel symbols as text, preserving their contents unchanged.</details> <!-- cr-comment:v1:9a00f424dd374b68cc27791a --> _Source: Linters/SAST tools_ </blockquote></details> <details> <summary>components/support/SupportPanelHost.vue (1)</summary><blockquote> `20-33`: _🎯 Functional Correctness_ | _🔵 Trivial_ | _⚡ Quick win_ **Add `aria-modal` and basic focus management to the support dialog.** The panel is marked `role="dialog"` but lacks `aria-modal="true"`, and focus isn't moved into the panel on open or restored to the trigger on close. <details> <summary>♻️ Proposed fix</summary> ```diff <div v-if="isOpen" ref="panelRef" class="fixed right-24 bottom-24 z-[2900] mobile:inset-16" role="dialog" + aria-modal="true" aria-label="Support" >🤖 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 `@components/support/SupportPanelHost.vue` around lines 20 - 33, Update the dialog element in SupportPanelHost’s template to include aria-modal="true", then add focus management around the isOpen state: move focus to panelRef when the support panel opens and restore focus to the trigger element when it closes. Reuse the existing refs or add dedicated trigger/panel refs and lifecycle/watch logic within SupportPanelHost.components/support/SupportPanel.vue (2)
161-168: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winConsider sanitizing article HTML before
v-html, as defense-in-depth.The trust boundary (own HelpScout Docs collection, fetched server-side) is reasonable and documented, but rendering raw remote content via
v-htmlhas no fallback if a Docs editor account or the Docs API response is ever compromised. Running it through DOMPurify server- or client-side would cost little and remove the single point of failure.🤖 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 `@components/support/SupportPanel.vue` around lines 161 - 168, Sanitize the HelpScout article content before rendering it in the v-html binding within SupportPanel, using the project’s existing DOMPurify or HTML-sanitization utility if available. Apply sanitization to the article.text fallback expression while preserving the preview fallback and existing article display behavior.
181-215: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNo client-side feedback for server-enforced length/attachment limits.
Subject/body are silently truncated and excess/oversized attachments are silently dropped server-side (per
conversations.post.ts), with nothing surfaced to the user here. Consider addingmaxlengthon subject/body and a client-side attachment count/size check with an inline warning, so users aren't surprised their content was cut.Also applies to: 249-255
🤖 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 `@components/support/SupportPanel.vue` around lines 181 - 215, Update the SupportPanel subject and messageBody inputs to enforce the same maxlength limits as the server, and add client-side validation in the attachment handling/submission flow for maximum attachment count and size. Surface violations with an inline warning and prevent or clearly report truncated content or dropped attachments before submission.composables/useSupportPanel.ts (1)
64-99: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winGuard against stale async responses in
searchDocs/openArticle.Both functions unconditionally assign the fetch result to shared state on resolve, with no check that the request is still the most recent one. Fast typing or navigating away mid-fetch can let an older response overwrite newer results/article state.
♻️ Proposed guard for `searchDocs`
const searchDocs = async () => { const term = query.value.trim() if (term.length < 2) { results.value = [] return } isSearching.value = true error.value = '' try { - results.value = await $fetch<SupportArticle[]>('/api/internal/support/docs', { + const requestTerm = term + const fetched = await $fetch<SupportArticle[]>('/api/internal/support/docs', { query: { q: term }, }) + if (query.value.trim() === requestTerm) results.value = fetched } catch { - error.value = 'Search is unavailable right now.' - results.value = [] + if (query.value.trim() === term) { + error.value = 'Search is unavailable right now.' + results.value = [] + } } finally { isSearching.value = false } }🤖 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 `@composables/useSupportPanel.ts` around lines 64 - 99, Track the latest request for both searchDocs and openArticle, and only apply fetched results to results.value or article.value when the response belongs to the current request and view context. In searchDocs, invalidate prior searches when the query is too short or a newer search starts; in openArticle, invalidate prior article loads when navigating to another item, while preserving the existing loading, error, and preview behavior.
🤖 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 `@components/support/SupportPanel.vue`:
- Around line 23-26: Reset the compose fields after a successful send so a
subsequent “Start a conversation” flow begins empty. Update the success handling
in the send flow and the related sent/goHome transitions to clear subject,
messageBody, and attachments, while preserving emailInput and the existing
navigation behavior.
In `@server/api/internal/support/conversations.post.ts`:
- Line 24: Update the body-limit middleware to exempt the support conversation
upload route handled by readMultipartFormData, or apply a per-route limit that
permits the intended 10 MB attachment size while retaining the 1 MB default for
other API routes. Add a regression test covering a multipart upload larger than
1 MB but within the support limit.
In `@server/api/internal/support/docs.get.ts`:
- Around line 21-58: Update the single-article path in the handler around
docsFetch('/articles/' + encodeURIComponent(query.id)) to require the configured
collectionId and verify the fetched article belongs to that collection before
returning its text. Reject missing or mismatched collection membership as an
upstream/invalid article response, while preserving the existing response for
articles scoped to collectionId.
In `@server/utils/helpscout.ts`:
- Around line 30-36: Update isSupportConfigured to validate mailboxId as a
positive safe integer rather than only checking truthiness; retain the existing
appId and appSecret checks. Add regression coverage for Infinity, negative, and
fractional mailbox IDs, ensuring they are reported as unconfigured.
---
Nitpick comments:
In `@components/support/SupportPanel.vue`:
- Around line 161-168: Sanitize the HelpScout article content before rendering
it in the v-html binding within SupportPanel, using the project’s existing
DOMPurify or HTML-sanitization utility if available. Apply sanitization to the
article.text fallback expression while preserving the preview fallback and
existing article display behavior.
- Around line 181-215: Update the SupportPanel subject and messageBody inputs to
enforce the same maxlength limits as the server, and add client-side validation
in the attachment handling/submission flow for maximum attachment count and
size. Surface violations with an inline warning and prevent or clearly report
truncated content or dropped attachments before submission.
In `@components/support/SupportPanelHost.vue`:
- Around line 20-33: Update the dialog element in SupportPanelHost’s template to
include aria-modal="true", then add focus management around the isOpen state:
move focus to panelRef when the support panel opens and restore focus to the
trigger element when it closes. Reuse the existing refs or add dedicated
trigger/panel refs and lifecycle/watch logic within SupportPanelHost.
In `@composables/useSupportPanel.ts`:
- Around line 64-99: Track the latest request for both searchDocs and
openArticle, and only apply fetched results to results.value or article.value
when the response belongs to the current request and view context. In
searchDocs, invalidate prior searches when the query is too short or a newer
search starts; in openArticle, invalidate prior article loads when navigating to
another item, while preserving the existing loading, error, and preview
behavior.
In `@docs/support-panel.md`:
- Around line 19-25: Update the fenced code blocks in the support panel
documentation to include language identifiers: mark the environment-variable
block as env and the file-list block around the referenced support-panel symbols
as text, preserving their contents unchanged.
🪄 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: euler-xyz/coderabbit/.coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8bf674be-f762-41ec-8884-b9442a8ad458
📒 Files selected for processing (14)
app.vueassets/styles/main.scsscomponents/layout/TheHeader.vuecomponents/support/SupportPanel.vuecomponents/support/SupportPanelHost.vuecomposables/useDeployConfig.tscomposables/useSupportPanel.tsdocs/support-panel.mdnuxt.config.tsserver/api/internal/support/conversations.post.tsserver/api/internal/support/docs.get.tsserver/utils/helpscout.tstailwind.config.jstests/server/helpscout-support.test.ts
| const subject = ref('') | ||
| const messageBody = ref('') | ||
| const emailInput = ref(email.value) | ||
| const attachments = ref<File[]>([]) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Compose fields aren't reset after a successful send.
subject, messageBody, and attachments persist across a sent → goHome → "Start a conversation" cycle within the same panel session, so a second ticket opens pre-filled with the previous message.
🐛 Proposed fix
-const onSend = () => send({
- subject: subject.value,
- body: messageBody.value,
- email: emailInput.value,
- attachments: attachments.value,
-})
+const onSend = async () => {
+ await send({
+ subject: subject.value,
+ body: messageBody.value,
+ email: emailInput.value,
+ attachments: attachments.value,
+ })
+ if (view.value === 'sent') {
+ subject.value = ''
+ messageBody.value = ''
+ attachments.value = []
+ }
+}Also applies to: 56-61, 309-312, 352-355
🤖 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 `@components/support/SupportPanel.vue` around lines 23 - 26, Reset the compose
fields after a successful send so a subsequent “Start a conversation” flow
begins empty. Update the success handling in the send flow and the related
sent/goHome transitions to clear subject, messageBody, and attachments, while
preserving emailInput and the existing navigation behavior.
| throw createError({ statusCode: 503, statusMessage: 'Support is not configured' }) | ||
| } | ||
|
|
||
| const parts = await readMultipartFormData(event) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n --glob 'package.json' --glob '*lock*' '"h3"|"nitro"|"nuxt"' .
rg -nP 'assertBodySize|bodyLimit|maxRequestBodySize|readMultipartFormData' \
nuxt.config.ts server tests 2>/dev/null || trueRepository: euler-xyz/euler-lite
Length of output: 1268
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== server/api/internal/support/conversations.post.ts ==\n'
cat -n server/api/internal/support/conversations.post.ts
printf '\n== nuxt.config.ts ==\n'
if [ -f nuxt.config.ts ]; then cat -n nuxt.config.ts; else echo 'nuxt.config.ts not found'; fi
printf '\n== package.json (relevant deps) ==\n'
python3 - <<'PY'
import json
from pathlib import Path
p = json.loads(Path('package.json').read_text())
for k in ['nuxt','h3','nitro']:
print(k, p.get('dependencies', {}).get(k) or p.get('devDependencies', {}).get(k))
PY
printf '\n== package-lock references ==\n'
rg -n '"h3"|\"nitro\"|\"nuxt\"|assertBodySize|bodyLimit|maxRequestBodySize|readMultipartFormData' package-lock.json nuxt.config.ts server tests 2>/dev/null || trueRepository: euler-xyz/euler-lite
Length of output: 26259
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import json
from pathlib import Path
lock = json.loads(Path('package-lock.json').read_text())
# Try to extract resolved versions for top-level and direct dependencies.
print('lockfileVersion', lock.get('lockfileVersion'))
pkgs = lock.get('packages', {})
for path in ['', 'node_modules/h3', 'node_modules/nitro', 'node_modules/nuxt']:
if path in pkgs:
info = pkgs[path]
print(path or 'root', 'version=', info.get('version'))
PYRepository: euler-xyz/euler-lite
Length of output: 261
🌐 Web query:
h3 1.15.11 assertBodySize request body size streaming limit docs
💡 Result:
In the h3 framework (as of version 1.15.11), assertBodySize is a utility designed to enforce a maximum request body size limit [1][2]. When the request body exceeds the specified limit, it throws a 413 Request Entity Too Large error [1][2]. Key characteristics of assertBodySize and request body streaming in h3 include: Streaming Enforcement: Modern versions of h3 (notably following improvements in PR #1500) use stream-based enforcement [3]. Instead of pre-buffering the entire request body to check its size—which would hold data in memory and block streaming handlers—the framework wraps the request body stream [3]. This allows the size to be counted byte-by-byte as the handler consumes the stream, ensuring that streaming handlers can start immediately without unnecessary latency [3]. Mechanism: 1. Header Checks: The utility performs an initial check against the Content-Length header if present to fail-fast [1][3]. 2. Stream Wrapping: It wraps event.req.body with a limiting stream, which counts bytes during consumption [3]. 3. Request Rebuilding: It swaps in a rebuilt, limited native Request object, ensuring that subsequent calls (like readBody or stream iteration) respect the limit [3]. Security and Edge Cases: - The utility is designed to handle potential RFC 7230 header issues, such as throwing a 400 error if both Content-Length and Transfer-Encoding headers are present [1][3][4]. - Recent updates have improved the consistency of enforcement, ensuring the stream-based size check is applied regardless of whether a Content-Length header is provided [5]. For developers, usage typically involves calling the function at the beginning of an event handler: async (event) => { await assertBodySize(event, 10 * 1024 * 1024); // 10MB limit // Proceed with stream consumption } [1][2]
Citations:
- 1: https://github.com/unjs/h3/blob/main/src/utils/body.ts
- 2: https://h3.dev/utils/request
- 3: perf(body-limit): stream enforcement instead of pre-buffering h3js/h3#1500
- 4: h3js/h3@9ccd301
- 5: h3js/h3@708a3aa
🌐 Web query:
Nuxt 4 h3 bodyLimit maxRequestBodySize readMultipartFormData request size limit
💡 Result:
In Nuxt 4 (powered by Nitro and h3), request body size limits are managed through Nitro route rules or specific middleware, as h3 does not have a global, out-of-the-box size limit enforced by default in all environments [1][2]. To configure or increase the request size limit, use the bodySizeLimit property within your nuxt.config.ts under nitro.routeRules [1]. This is the recommended approach for defining limits per route, which helps prevent memory exhaustion while allowing larger uploads where necessary [1]. Example configuration in nuxt.config.ts: export default defineNuxtConfig({ nitro: { routeRules: { '/api/upload': { // Set limit in bytes (e.g., 10MB) bodySizeLimit: 10485760 } } } }) Additionally, if you are using the nuxt-security module, it provides a requestSizeLimiter middleware that allows for global or per-route configuration [3]. This middleware can be configured to manage both general request sizes and specific upload file sizes [3]. Important considerations: 1. Underlying Platform Limits: Be aware that your deployment environment (e.g., Nginx, Cloudflare, or specific serverless providers) may impose its own request body size limits [1][4][5]. Even if you increase the limit in Nitro, you may encounter 413 Payload Too Large errors if the load balancer or proxy server (like Nginx) restricts the payload size [1][4]. 2. Memory Usage: Because h3 (and the underlying readMultipartFormData or native FormData parsing) often buffers the request body into memory, allowing very large bodySizeLimit values can lead to Out-Of-Memory (OOM) errors [6][7][8]. For very large file uploads, it is generally recommended to use techniques such as pre-signed URLs (uploading directly to cloud storage) or specialized streaming libraries rather than parsing the entire file through the Nuxt/Nitro server [7][8]. 3. h3 Utilities: h3 provides an assertBodySize(event, limit) utility that you can call manually within your server handlers to enforce size constraints at specific points in your application logic [9][10][2]. Top results: [1], [3], [9], [2]
Citations:
- 1: https://javascript-news.org/fixing-nuxt-3-file-uploads-without-losing-your-mind
- 2: feat: add
assertBodySizeutil andbodyLimitmiddleware h3js/h3#1222 - 3: https://nuxt-security.vercel.app/middleware/request-size-limiter
- 4: Upload limit on deployed app - 413 error nuxt/nuxt#19216
- 5: 413 Payload Too Large Error on Uploading Images Larger Than 2MB Using NuxtHub with Cloudflare nuxt-hub/core#364
- 6: Handle large bodies h3js/h3#514
- 7: readMultipartFormData error when parsing big files h3js/h3#851
- 8: support super large file uploads (50-100GB) h3js/h3#1155
- 9: https://h3.dev/utils/more
- 10: https://github.com/unjs/h3/blob/main/src/utils/body.ts
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== file list for config-ish files ==\n'
git ls-files | rg '(^|/)(nuxt\.config\.(ts|js|mjs|cjs)|nitro\.config\.(ts|js|mjs|cjs)|app\.config\.(ts|js|mjs|cjs)|server/|tests/)' || true
printf '\n== global request-size limit search ==\n'
rg -n 'bodySizeLimit|maxRequestBodySize|bodyLimit|requestSize|assertBodySize|readMultipartFormData' .
printf '\n== conversations route ==\n'
cat -n server/api/internal/support/conversations.post.tsRepository: euler-xyz/euler-lite
Length of output: 14866
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== server/middleware/body-limit.ts ==\n'
cat -n server/middleware/body-limit.ts
printf '\n== any references to body-limit middleware ==\n'
rg -n 'body-limit|bodySizeLimit|assertBodySize|maxRequestBodySize|readMultipartFormData' server nuxt.config.ts tests .
printf '\n== middleware file list ==\n'
git ls-files 'server/middleware/*' 'server/plugins/*' | sortRepository: euler-xyz/euler-lite
Length of output: 4542
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('server/middleware/body-limit.ts')
print(p.exists())
if p.exists():
text = p.read_text()
print('lines', text.count('\n') + 1)
print(text)
PYRepository: euler-xyz/euler-lite
Length of output: 1977
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check whether the support route or its directory opts out of middleware behavior.
rg -n 'defineEventHandler|export default defineEventHandler|middleware' server/api/internal/support server/middlewareRepository: euler-xyz/euler-lite
Length of output: 873
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== server/middleware/body-limit.ts ==\n'
cat -n server/middleware/body-limit.ts
printf '\n== search for request-size enforcement ==\n'
rg -n 'body-limit|bodySizeLimit|assertBodySize|maxRequestBodySize|readMultipartFormData' server nuxt.config.ts tests .Repository: euler-xyz/euler-lite
Length of output: 4047
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== support docs ==\n'
rg -n 'attachment|HelpScout|support panel|MAX_ATTACHMENT_BYTES|MAX_ATTACHMENTS|conversations.post' docs server tests
printf '\n== helpscout tests ==\n'
sed -n '1,260p' tests/server/helpscout-support.test.ts
printf '\n== architecture body-limit note ==\n'
sed -n '330,360p' docs/architecture.mdRepository: euler-xyz/euler-lite
Length of output: 8033
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== any route-specific body limit overrides ==\n'
rg -n 'bodySizeLimit|maxRequestBodySize|requestSizeLimiter|413|Payload Too Large|Length Required' server docs tests nuxt.config.ts package.json
printf '\n== support route neighbors ==\n'
sed -n '1,220p' server/api/internal/support/docs.get.tsRepository: euler-xyz/euler-lite
Length of output: 40708
Raise the support upload body cap.
server/middleware/body-limit.ts rejects all /api/* requests above 1 MB before readMultipartFormData() runs, so the 10 MB attachment cap here is unreachable. Add a support-route exception or per-route override and a regression test for oversized multipart uploads.
🤖 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 `@server/api/internal/support/conversations.post.ts` at line 24, Update the
body-limit middleware to exempt the support conversation upload route handled by
readMultipartFormData, or apply a per-route limit that permits the intended 10
MB attachment size while retaining the 1 MB default for other API routes. Add a
regression test covering a multipart upload larger than 1 MB but within the
support limit.
| const { docsKey, collectionId } = helpScoutConfig() | ||
|
|
||
| // Search is unavailable rather than broken when the key is absent, so the | ||
| // panel can still be used to open a ticket. | ||
| if (!docsKey) throw createError({ statusCode: 503, statusMessage: 'Docs search is not configured' }) | ||
|
|
||
| // Single-article mode — used when the panel opens an article from a result row. | ||
| if (typeof query.id === 'string' && query.id) { | ||
| if (!/^[\w-]{1,40}$/.test(query.id)) { | ||
| throw createError({ statusCode: 400, statusMessage: 'Invalid article id' }) | ||
| } | ||
| try { | ||
| const data = await docsFetch<DocsArticleResponse>('/articles/' + encodeURIComponent(query.id)) | ||
| setResponseHeader(event, 'Cache-Control', 'private, max-age=300') | ||
| return { id: data.article.id, name: data.article.name, preview: '', text: data.article.text } | ||
| } | ||
| catch { | ||
| throw createError({ statusCode: 502, statusMessage: 'Upstream error' }) | ||
| } | ||
| } | ||
|
|
||
| const term = typeof query.q === 'string' ? query.q.trim() : '' | ||
| if (term.length < 2) return [] | ||
| if (term.length > 120) throw createError({ statusCode: 400, statusMessage: 'Query too long' }) | ||
|
|
||
| try { | ||
| const data = await docsFetch<DocsSearchResponse>('/search/articles', { | ||
| query: term, | ||
| collectionId, | ||
| status: 'published', | ||
| pageSize: '8', | ||
| }) | ||
| setResponseHeader(event, 'Cache-Control', 'private, max-age=60') | ||
| return data.articles.items.map(item => ({ | ||
| id: item.id, | ||
| name: item.name, | ||
| preview: item.preview, | ||
| })) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## files\n'
git ls-files 'server/api/internal/support/docs.get.ts' 'server/**/helpScout*' 'server/**/docs*' 'server/**/support*' | sed 's#^`#-` #'
printf '\n## outline docs.get.ts\n'
ast-grep outline server/api/internal/support/docs.get.ts --view expanded || true
printf '\n## outline potential helper files\n'
for f in $(git ls-files 'server/**/helpScout*' 'server/**/docs*' 'server/**/support*' | head -n 20); do
echo "### $f"
ast-grep outline "$f" --view expanded || true
doneRepository: euler-xyz/euler-lite
Length of output: 1356
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## docs.get.ts excerpt\n'
nl -ba server/api/internal/support/docs.get.ts | sed -n '1,220p'
printf '\n## search for helpScoutConfig/docsFetch definitions\n'
rg -n "function helpScoutConfig|const helpScoutConfig|docsFetch<|docsFetch\\(" server -g'*.ts' -g'*.js' -g'*.tsx' -g'*.jsx' || trueRepository: euler-xyz/euler-lite
Length of output: 222
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## docs.get.ts\n'
sed -n '1,220p' server/api/internal/support/docs.get.ts
printf '\n## search for docsFetch and helpScoutConfig\n'
rg -n "docsFetch|helpScoutConfig|collectionId|status: 'published'|pageSize: '8'" server -g'*.ts' -g'*.js' -g'*.tsx' -g'*.jsx' || trueRepository: euler-xyz/euler-lite
Length of output: 4033
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' server/utils/helpscout.tsRepository: euler-xyz/euler-lite
Length of output: 5481
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('server/utils/helpscout.ts')
print(p.read_text())
PYRepository: euler-xyz/euler-lite
Length of output: 5482
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,200p' server/utils/helpscout.tsRepository: euler-xyz/euler-lite
Length of output: 5481
Enforce collection scoping on article reads. Search already limits to published, but the single-article path still returns any readable article ID without checking it belongs to the configured collection. Require collectionId and reject out-of-collection articles before returning text.
🤖 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 `@server/api/internal/support/docs.get.ts` around lines 21 - 58, Update the
single-article path in the handler around docsFetch('/articles/' +
encodeURIComponent(query.id)) to require the configured collectionId and verify
the fetched article belongs to that collection before returning its text. Reject
missing or mismatched collection membership as an upstream/invalid article
response, while preserving the existing response for articles scoped to
collectionId.
| mailboxId: Number(process.env.HELPSCOUT_MAILBOX_ID ?? 0), | ||
| }) | ||
|
|
||
| /** True when the Mailbox API credentials needed to open a ticket are present. */ | ||
| export const isSupportConfigured = () => { | ||
| const { appId, appSecret, mailboxId } = helpScoutConfig() | ||
| return !!appId && !!appSecret && !!mailboxId |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Require a valid mailbox ID.
Number('Infinity'), -1, and 1.5 are truthy, so this marks invalid configuration as ready and the endpoint later fails upstream. Require a positive safe integer; add regression cases for these values.
Proposed fix
export const isSupportConfigured = () => {
const { appId, appSecret, mailboxId } = helpScoutConfig()
- return !!appId && !!appSecret && !!mailboxId
+ return !!appId && !!appSecret
+ && Number.isSafeInteger(mailboxId)
+ && mailboxId > 0
}📝 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.
| mailboxId: Number(process.env.HELPSCOUT_MAILBOX_ID ?? 0), | |
| }) | |
| /** True when the Mailbox API credentials needed to open a ticket are present. */ | |
| export const isSupportConfigured = () => { | |
| const { appId, appSecret, mailboxId } = helpScoutConfig() | |
| return !!appId && !!appSecret && !!mailboxId | |
| mailboxId: Number(process.env.HELPSCOUT_MAILBOX_ID ?? 0), | |
| }) | |
| /** True when the Mailbox API credentials needed to open a ticket are present. */ | |
| export const isSupportConfigured = () => { | |
| const { appId, appSecret, mailboxId } = helpScoutConfig() | |
| return !!appId && !!appSecret | |
| && Number.isSafeInteger(mailboxId) | |
| && mailboxId > 0 |
🤖 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 `@server/utils/helpscout.ts` around lines 30 - 36, Update isSupportConfigured
to validate mailboxId as a positive safe integer rather than only checking
truthiness; retain the existing appId and appSecret checks. Add regression
coverage for Infinity, negative, and fractional mailbox IDs, ensuring they are
reported as unconfigured.
Drop configEnableSupportPanel: the panel and its Support entry in the header Resources menu are always present. Nothing about the panel itself changes. The Beacon bubble stays visible rather than being hidden with the flag. The panel cannot deliver a ticket until the HELPSCOUT_* credentials are configured, so retiring the bubble now would leave no working support channel. main.scss documents the one-line change to hide it once the credentials land. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reverts 8942a71 and be15391. The native panel duplicated a widget HelpScout already provides and could not deliver a ticket without credentials that do not exist yet, so it went back out. Beacon is now themed through its own config API instead — its UI is a cross-origin iframe, so this is the only surface our code can reach: - launcher takes the app's accent per theme (#2ae5b9 dark, #1c997c light) with the question icon, re-applied when the theme changes - zIndex 2500 keeps it above content but under UiModal (3000) - vertical offset lifts it clear of the ~98px mobile bottom nav, and updates on resize - the reply-time label states that the wallet and diagnostics are attached, where the user can see it but not edit it Also restores the Beacon wiring lost in an earlier merge: wallet address, chain, app state and the console ring buffer go out as session-data, re-snapshotted when the panel opens, and the launcher is hidden on the onboarding screen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The launcher was using --accent-500 (#2ae5b9), the bright text accent, which read louder than every button in the app. Beacon's brand colour now comes from --accent-600 (#23c09b) read off the document at runtime, so it matches the primary button exactly instead of being a hardcoded copy that can drift. Beacon's config API only reaches the brand colour, icon and labels — the dialog's own surfaces stayed white on a dark app. Its iframes carry no src and are same-origin, so useBeaconTheme injects a stylesheet into them built from --bg-card, --bg-surface, --text-*, --border-default and --accent-600, re-resolved on theme switch and re-applied when Beacon recreates the panel. Selectors match HelpScout's styled-components class prefixes. If they rename a component that rule stops applying and the dialog falls back to their stock styling rather than breaking; frame access is guarded so a future cross-origin iframe would no-op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The channel chooser and the previous-conversations list ship their own white cards, unreadable on the dark app. Those screens cannot be opened on a local Beacon (no prior conversations, single channel), so their class names cannot be verified — chasing them was the wrong approach. Instead, after the stylesheet is applied, scan the frame for anything still painting a light background and give it --bg-card. Near-greyscale dark text inside those surfaces is lifted to --text-secondary; text with saturated colour (the green "Received" marker, links) is left as Beacon intended. In light themes --bg-card is white, so this is a no-op there. Fixes two bugs in the previous pass: the frame observer watched the host document, so Beacon's internal re-renders never retriggered theming; and once switched to the frame document, replacing the <style> element was itself a childList mutation, which looped and hung the tab. The stylesheet is now created once and rewritten only when it differs, with a re-entrancy guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The conversation thread showed a white panel with unreadable light text. The white was not an element we could repaint: Beacon leaves html and body transparent, so wherever its container does not cover the whole frame the browser paints the default canvas white, and our lifted text landed on it. Painting html/body with --bg-card fixes every screen at once, including the ones that cannot be opened locally, rather than needing a rule per container component. No-op in light themes, where --bg-card is white. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lp_scout_integration


Embed the official Beacon loader snippet as a nonce-carrying inline script (bodyClose) in nuxt.config.ts and init it with our Beacon ID. Allow the required HelpScout origins in the CSP (connect/style/font/ frame/media-src) per their documented requirements; object-src stays 'none'. Add a regression test locking the connect-src entries.
Summary by CodeRabbit
New Features
Security
Documentation
Tests