Skip to content

add HelpScout Beacon integration - #705

Open
girts512 wants to merge 16 commits into
developmentfrom
feature/add_help_scout_integration
Open

girts512 wants to merge 16 commits into
developmentfrom
feature/add_help_scout_integration

Conversation

@girts512

@girts512 girts512 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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

    • Added an in-app Support panel with searchable HelpScout docs and ticket sending with attachments.
    • Enabled HelpScout Beacon with deferred loading, CSP nonce support, and an option to hide the Beacon.
    • Added support diagnostics collection that captures recent console output and errors.
  • Security

    • Updated CSP to allow required HelpScout/Beacon resources and connections.
    • Redacted sensitive query-string values in captured diagnostics.
  • Documentation

    • Added documentation for enabling and operating the Support panel.
  • Tests

    • Extended security and HelpScout support coverage, plus console-capture behavior tests.

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>
@railway-app
railway-app Bot temporarily deployed to euler-lite / euler-lite-pr-705 July 8, 2026 11:12 Destroyed
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Adds 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.

Changes

HelpScout Support Integration

Layer / File(s) Summary
Beacon, CSP, and diagnostics
nuxt.config.ts, types/index.ts, server/plugins/csp.ts, assets/styles/main.scss, utils/console-capture.ts, plugins/00.console-capture.client.ts, tests/server/security.test.ts, tests/utils/console-capture.test.ts
Loads Beacon after window load, expands CSP permissions, controls Beacon visibility, and captures bounded, redacted console and browser error output.
Server-side HelpScout APIs
server/utils/helpscout.ts, server/api/internal/support/*, tests/server/helpscout-support.test.ts
Adds authenticated Docs and Mailbox clients, token caching, article search/read endpoints, conversation creation, validation, rate limiting, diagnostics, attachments, and helper tests.
Support state and UI flow
composables/useDeployConfig.ts, composables/useSupportPanel.ts, components/support/*, components/layout/TheHeader.vue, app.vue, assets/styles/main.scss, tailwind.config.js
Adds feature gating, shared support state, article browsing, conversation composition, sent confirmation, header access, modal dismissal behavior, Beacon hiding, and component styling support.
Feature documentation
docs/support-panel.md
Documents configuration, server-side integration, diagnostics, scope limits, compliance considerations, and API shape verification points.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested reviewers: dglowinski

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding HelpScout Beacon integration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/add_help_scout_integration

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
nuxt.config.ts (1)

7-11: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Beacon 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 static innerHTML string, this would require building the snippet from useRuntimeConfig().public at 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

📥 Commits

Reviewing files that changed from the base of the PR and between dda114d and e8c36e2.

📒 Files selected for processing (3)
  • nuxt.config.ts
  • server/plugins/csp.ts
  • tests/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>
@railway-app
railway-app Bot temporarily deployed to euler-lite / euler-lite-pr-705 July 8, 2026 11:26 Destroyed
@railway-app

railway-app Bot commented Jul 8, 2026

Copy link
Copy Markdown

🚅 Deployed to the euler-lite-pr-705 environment in euler-lite(dev,PR previews)

Service Status Web Updated (UTC)
dev-build ✅ Success (View Logs) Web Jul 31, 2026 at 9:56 am

girts512 and others added 2 commits July 14, 2026 09:42
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>
@railway-app
railway-app Bot temporarily deployed to euler-lite / euler-lite-pr-705 July 14, 2026 06:52 Destroyed
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 LeonardEulerXYZ left a comment

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.

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 every log/info/warn/error/debug argument, while QUERY_VALUE_RE masks 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 from app.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 — passed
  • npm 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):

Desktop HelpScout Beacon open

Mobile — HelpScout form open on Explore (390×844):

Mobile HelpScout Beacon open

Comment thread utils/console-capture.ts
const text = args
.map(stringifyArg)
.join(' ')
.replace(QUERY_VALUE_RE, '$1[redacted]')

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.

🔴 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.

@railway-app
railway-app Bot temporarily deployed to euler-lite(dev,PR previews) / euler-lite-pr-705 July 30, 2026 07:17 Destroyed
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>
@railway-app
railway-app Bot temporarily deployed to euler-lite(dev,PR previews) / euler-lite-pr-705 July 30, 2026 12:44 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
docs/support-panel.md (1)

19-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add 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.md around 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 win

Consider 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-html has 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 win

No 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 adding maxlength on 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 win

Guard 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

📥 Commits

Reviewing files that changed from the base of the PR and between 120e9ad and 8942a71.

📒 Files selected for processing (14)
  • app.vue
  • assets/styles/main.scss
  • components/layout/TheHeader.vue
  • components/support/SupportPanel.vue
  • components/support/SupportPanelHost.vue
  • composables/useDeployConfig.ts
  • composables/useSupportPanel.ts
  • docs/support-panel.md
  • nuxt.config.ts
  • server/api/internal/support/conversations.post.ts
  • server/api/internal/support/docs.get.ts
  • server/utils/helpscout.ts
  • tailwind.config.js
  • tests/server/helpscout-support.test.ts

Comment thread components/support/SupportPanel.vue Outdated
Comment on lines +23 to +26
const subject = ref('')
const messageBody = ref('')
const emailInput = ref(email.value)
const attachments = ref<File[]>([])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Compose fields aren't reset after a successful send.

subject, messageBody, and attachments persist across a sentgoHome → "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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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 || true

Repository: 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 || true

Repository: 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'))
PY

Repository: 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:


🌐 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:


🏁 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.ts

Repository: 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/*' | sort

Repository: 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)
PY

Repository: 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/middleware

Repository: 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.md

Repository: 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.ts

Repository: 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.

Comment thread server/api/internal/support/docs.get.ts Outdated
Comment on lines +21 to +58
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,
}))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 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
done

Repository: 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' || true

Repository: 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' || true

Repository: euler-xyz/euler-lite

Length of output: 4033


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' server/utils/helpscout.ts

Repository: 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())
PY

Repository: euler-xyz/euler-lite

Length of output: 5482


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,200p' server/utils/helpscout.ts

Repository: 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.

Comment thread server/utils/helpscout.ts Outdated
Comment on lines +30 to +36
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
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>
@railway-app
railway-app Bot temporarily deployed to euler-lite(dev,PR previews) / euler-lite-pr-705 July 30, 2026 12:54 Destroyed
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>
@railway-app
railway-app Bot temporarily deployed to euler-lite(dev,PR previews) / euler-lite-pr-705 July 30, 2026 13:05 Destroyed
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>
@railway-app
railway-app Bot temporarily deployed to euler-lite(dev,PR previews) / euler-lite-pr-705 July 30, 2026 13:39 Destroyed
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>
@railway-app
railway-app Bot temporarily deployed to euler-lite(dev,PR previews) / euler-lite-pr-705 July 30, 2026 14:35 Destroyed
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>
@railway-app
railway-app Bot temporarily deployed to euler-lite(dev,PR previews) / euler-lite-pr-705 July 30, 2026 14:55 Destroyed
@railway-app
railway-app Bot temporarily deployed to euler-lite(dev,PR previews) / euler-lite-pr-705 July 31, 2026 07:21 Destroyed
@railway-app
railway-app Bot temporarily deployed to euler-lite(dev,PR previews) / euler-lite-pr-705 July 31, 2026 08:35 Destroyed
@railway-app
railway-app Bot temporarily deployed to euler-lite(dev,PR previews) / euler-lite-pr-705 July 31, 2026 08:45 Destroyed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants