Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .sync/log/f71bc4692d3fb0b7ef52ff91413bfee800586f8f.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Port: docs(Chat): validate AI theme and inputs

**Upstream:** `f71bc4692d3fb0b7ef52ff91413bfee800586f8f` (nuxt/ui)
**Decision:** port (partial — one of two files is a no-op in b24ui)

## Upstream change
Security hardening of the docs AI assistant (two docs-app files):
1. `docs/app/composables/useTheme.ts` — sanitize the untrusted AI `applyTheme`
output before it is persisted/concatenated into `<style>` rules: add
`sanitizeCustomColors` / `sanitizeCSSVariables` + `SAFE_*` regexes, validate
`primary`/`neutral`/`radius`/`font`/per-color tokens, and skip
`__proto__`/`constructor`/`prototype` keys when merging into `appConfig.ui`.
2. `docs/server/api/ai.post.ts` — `currentPage` is interpolated verbatim into
the system prompt, so it is an indirect prompt-injection surface; accept it
only as a plain `/docs/...` path (`length <= 128`, no `\r\n`,
`/^\/docs\/[\w/-]*$/`), else drop it (`safeCurrentPage`).

## b24ui adaptation
- **`docs/server/api/ai.post.ts` — ported 1:1.** b24ui interpolates
`currentPage` into its system prompt exactly like upstream pre-change (same
prompt-injection surface), and its client (`chat/Chat.vue`) already sends
`currentPage` only for `route.path.startsWith('/docs/')` — so the upstream
`/^\/docs\/[\w/-]*$/` guard fits b24ui's path scheme unchanged. Added the
`safeCurrentPage` server-side guard (defense-in-depth: a direct POST can
bypass the client check) and swapped `currentPage` → `safeCurrentPage` in the
prompt. Kept b24ui's `createError({ status: 400, … })` style.
- **`docs/app/composables/useTheme.ts` — no-op (nothing to attach to).** b24ui
has already **neutered** the AI theme application: `customColorsStyle` /
`cssVariablesStyle` are `computed(() => '')` (no injection) and
`applyThemeSettings(_settings)` is a stub (`track('AI Theme Applied')` only) —
it never calls `injectCustomColors`/`injectCSSVariables`, never assigns
`primary`/`neutral`/`radius`/`font`, and never mutates `appConfig.ui`. The
untrusted-value sink the upstream sanitizers protect does not exist here, so
no `useTheme.ts` change is warranted.

## Verification (pnpm 11.5.2, gate ON)
docs-only; dev:prepare · lint · typecheck — green. No `src/` change, no
snapshot churn.
10 changes: 8 additions & 2 deletions .sync/nuxt-ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"upstream": "nuxt/ui",
"branch": "v4",
"sync_enabled": false,
"cursor": "02259a6ae963408b2a971722b9b66d225a0505f3",
"cursor": "f71bc4692d3fb0b7ef52ff91413bfee800586f8f",
"_cursor_note": "cursor = last upstream commit ported into b24ui (oldest-first, manual cadence). sync_enabled stays false until Phase 2 (porter workflow #67 + CLAUDE_CODE_OAUTH_TOKEN) is wired and trusted. `processed` is maintained per port from now on (backfilled #68-#72 on 2026-06-09).",
"stats": {
"queue_depth": 0,
Expand Down Expand Up @@ -563,10 +563,16 @@
"summary": "feat(Calendar): add month and year selection (#6582) — SKIPPED (maintainer call): largest single-component change (Calendar.vue +296, calendar.ts +102, docs +226, ~4800-line snapshot regen). Component logic ports mechanically but the theme does not: upstream calendar.ts is built on theme.colors loops + nuxt/ui semantic tokens, while b24ui's calendar.ts is air-design-token based with no theme.colors — the new `view` variant, daySizes/pickerSizes compounds, month/year cellTrigger styling and headingLabel slot need a deliberate air-design pass, not a 1:1 translation. No upstream-dep blocker (reka-ui 2.9.10 already exposes Month/YearPicker namespaced, #193). Tracked in issue #194"
},
"02259a6ae963408b2a971722b9b66d225a0505f3": {
"pr": 196,
"b24ui_sha": "508cebb4",
"decision": "port",
"summary": "fix(CommandPalette): only scroll to highlighted item when focused (#6579) — CommandPalette.vue: guard the watch(filteredGroups) re-highlight with rootEl.contains(document.activeElement) — highlightFirstItem() only when the palette has focus, else highlightSelected(undefined, false) so async results (useLazyFetch) below the fold don't scroll the whole page. Direct 1:1 (b24ui matched the pre-change; same reka ListboxRoot API). +spec (results without focus re-highlight but don't scrollIntoView; with focus do). Builds on #76 (efd7b8ec)"
},
"f71bc4692d3fb0b7ef52ff91413bfee800586f8f": {
"pr": null,
"b24ui_sha": "pending-merge",
"decision": "port",
"summary": "fix(CommandPalette): only scroll to highlighted item when focused (#6579) — CommandPalette.vue: guard the watch(filteredGroups) re-highlight with rootEl.contains(document.activeElement) — highlightFirstItem() only when the palette has focus, else highlightSelected(undefined, false) so async results (useLazyFetch) below the fold don't scroll the whole page. Direct 1:1 (b24ui matched the pre-change; same reka ListboxRoot API). +spec (results without focus re-highlight but don't scrollIntoView; with focus do). Builds on #76 (efd7b8ec)"
"summary": "docs(Chat): validate AI theme and inputs — PARTIAL port (docs-only). ai.post.ts: ported 1:1 — currentPage is interpolated verbatim into the system prompt (same prompt-injection surface as upstream), added safeCurrentPage server-side guard (string, <=128, no \\r\\n, /^\\/docs\\/[\\w/-]*$/) and swapped currentPage->safeCurrentPage; matches b24ui client which only sends /docs/ paths. useTheme.ts: NO-OP — b24ui already neutered AI theme application (customColors/cssVariables styles are computed(()=>''), applyThemeSettings is a track-only stub, no injectCustomColors/appConfig.ui mutation), so the untrusted sink the upstream sanitizers protect doesn't exist. No src change"
}
}
}
13 changes: 12 additions & 1 deletion docs/server/api/ai.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ export default defineEventHandler(async (event) => {
throw createError({ status: 400, message: 'Invalid or missing messages array.' })
}

// `currentPage` is interpolated verbatim into the system prompt below, so it is an
// indirect prompt-injection surface (a crafted /docs/... link can smuggle newlines and
// instructions via Vue Router's path decoding). Accept it only when it is a plain docs
// path with no control characters; otherwise drop it.
const safeCurrentPage = typeof currentPage === 'string'
&& currentPage.length <= 128
&& !/[\r\n]/.test(currentPage)
&& /^\/docs\/[\w/-]*$/.test(currentPage)
? currentPage
: null

const mcpTools = mcpToolsToAiTools()

const deepseek = createDeepSeek({
Expand All @@ -60,7 +71,7 @@ export default defineEventHandler(async (event) => {
const system = `You are a helpful assistant for Bitrix24 UI, a UI library for Nuxt and Vue. Use your knowledge base tools to search for relevant information before answering questions.

The user is using **${framework === 'vue' ? 'Vue' : 'Nuxt'}**. Tailor your answers accordingly — ${framework === 'vue' ? 'use the Vite plugin setup, Vue Router, and vite.config.ts instead of Nuxt-specific features like modules or app.config.ts. IMPORTANT: The Vite plugin auto-imports components and Bitrix24 UI composables, but Vue core APIs and VueUse must be explicitly imported — always include these in code examples (e.g. `import { ref, computed } from \'vue\'`).' : 'use Nuxt modules, auto-imports, app.config.ts, and other Nuxt-specific features. Nuxt auto-imports Vue APIs (ref, computed, etc.), composables, and components — do not include these imports in code examples.'}
${currentPage ? `\nThe user is currently viewing the documentation page at \`${currentPage}\`. Use this context to provide more relevant answers (e.g. read that page first if the question seems related), but don't limit yourself to that page if the question is broader or unrelated.\n` : ''}
${safeCurrentPage ? `\nThe user is currently viewing the documentation page at \`${safeCurrentPage}\`. Use this context to provide more relevant answers (e.g. read that page first if the question seems related), but don't limit yourself to that page if the question is broader or unrelated.\n` : ''}
Guidelines:
- For documentation questions, ALWAYS use tools to search for information. Never rely on pre-trained knowledge for Bitrix24 UI APIs, props, or usage.
— When users ask you to apply a theme change in real time (e.g., "make it blue," "create a Sakura theme," "change the font"), tell them that the library style matches the Bitrix24 style and suggest using standard properties (color, variant).
Expand Down