Skip to content

Ai assistant widget#624

Open
uldisrudzitis wants to merge 5 commits intomasterfrom
ai-assistant
Open

Ai assistant widget#624
uldisrudzitis wants to merge 5 commits intomasterfrom
ai-assistant

Conversation

@uldisrudzitis
Copy link
Collaborator

@uldisrudzitis uldisrudzitis commented Mar 18, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added AI Assistant panel with chat interface for interactive conversations
    • Markdown rendering support for formatted content display
  • Documentation

    • Updated development guidelines with AI integration guidance
    • Added comprehensive skill guides for AI SDK development and testing frameworks
  • Updates

    • Upgraded PHP requirement to 8.3+
    • Updated Laravel ecosystem dependencies (Boost, Pint packages)
    • Integrated AI provider configuration support
    • Enhanced template system for improved content management

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 67d3ff31-2110-4b25-a584-18ccd581c938

📥 Commits

Reviewing files that changed from the base of the PR and between a68ff4e and 6a7ecf2.

📒 Files selected for processing (5)
  • _api_app/.codex/config.toml.example
  • _api_app/.cursor/mcp.json.example
  • _api_app/.gitignore
  • _api_app/.mcp.json.example
  • _api_app/opencode.json.example

📝 Walkthrough

Walkthrough

This pull request introduces comprehensive AI assistant functionality to the Berta CMS platform. It adds a Laravel-based AI SDK integration with configuration, a backend API endpoint for AI chat, an Angular-based chat UI component with NGXS state management, comprehensive test coverage, and supporting documentation and configuration files across multiple development tools (Claude, Cursor, Codex).

Changes

Cohort / File(s) Summary
AI Assistant Documentation & Guidelines
CLAUDE.md, _api_app/CLAUDE.md, _api_app/AGENTS.md, _api_app/.agents/skills/.../*, _api_app/.claude/skills/.../*, _api_app/.cursor/skills/.../*, _api_app/.github/skills/.../*
Introduces new documentation covering AI SDK development and Pest testing skills. Replaces WARP.md. Updates CLAUDE.md and AGENTS.md with skill activation, Laravel AI SDK integration details, and expanded conventions guidance.
Laravel Composer & Dependency Updates
_api_app/composer.json, _api_app/boost.json
Adds laravel/ai ^0.3.0 to dependencies, updates PHP requirement to ^8.3, upgrades laravel/boost to ^2.0 and laravel/pint to ^1.29. Adds boost.json configuration with AI agents, guidelines, and skills setup.
AI Configuration & Environment
_api_app/config/ai.php, _api_app/.env.example, _api_app/.codex/config.toml.example, _api_app/.cursor/mcp.json.example, _api_app/.mcp.json.example, _api_app/opencode.json.example
Introduces new ai.php config with provider defaults, caching settings, and 14 AI provider mappings (anthropic, azure, cohere, deepseek, etc.). Updates environment variables and adds MCP server configurations for multiple AI tools and IDE integrations.
Backend State & Controllers
_api_app/app/Http/Controllers/StateController.php, _api_app/app/User/UserModel.php
Adds conditional AI chat route to StateController's state URL map. Adds return type annotation to getLocaleSettings. Extends UserModel to conditionally include ai_assistant feature based on profile or plan existence.
Backend Middleware & Configuration
_api_app/app/Http/Middleware/Authenticate.php, _api_app/app/Http/Middleware/SetupMiddleware.php, _api_app/config/auth.php, _api_app/config/sanctum.php, _api_app/config/twigbridge.php, _api_app/bootstrap/providers.php, _api_app/database/factories/UserFactory.php, _api_app/tests/Pest.php
Refactors to use imported class names instead of fully-qualified references throughout. Removes unused imports and commented code from Authenticate.php. Simplifies PHPDoc type annotations in SetupMiddleware.php.
AI Chat Backend Testing
_api_app/tests/Feature/AiChatControllerTest.php
Adds comprehensive PHPUnit/Pest test suite (163 lines) covering authentication, JSON parsing, change enrichment, change history formatting, help articles generation, and AI response handling with fake AssistantAgent.
Angular AI Assistant Module
editor/src/app/ai-assistant/ai-assistant.actions.ts, editor/src/app/ai-assistant/ai-assistant.module.ts, editor/src/app/ai-assistant/ai-assistant.service.ts, editor/src/app/ai-assistant/ai-assistant.state.ts, editor/src/app/ai-assistant/ai-assistant.component.ts
Introduces complete AI assistant feature: four typed Redux-style actions (Toggle, SendMessage, MessageReceived, Clear), injectable service for HTTP communication with aiChat endpoint, NGXS state management with handlers for chat flow and change application, reusable module, and UI component with message history, input handling, and scroll/focus behavior.
Angular UI Integration
editor/src/app/app.module.ts, editor/src/app/app.component.ts, editor/src/app/header/header.component.ts
Integrates AI assistant module and state into root app, adds ai-assistant component to template, exposes isAiOpen$ observable and toggleAiAssistant() handler in header with conditional AI nav link based on user features.
Angular Pipes & Markdown
editor/src/app/pipes/markdown.pipe.ts, editor/src/app/pipes/pipes.module.ts
Adds new MarkdownPipe using marked library to convert markdown strings to sanitized HTML with external link handling. Wraps both SafePipe and MarkdownPipe in dedicated PipesModule.
Angular Template Re-render Services
editor/src/app/rerender/template-rerender.service.ts, editor/src/app/rerender/default-template-rerender.service.ts, editor/src/app/rerender/mashup/mashup-template-rerender.service.ts, editor/src/app/rerender/messy/messy-template-rerender.service.ts, editor/src/app/rerender/white/white-template-rerender.service.ts, editor/src/app/rerender/types/components.ts
Adds siteTexts setting group to re-render configuration across all template variants. Introduces SITE_TEXTS constant and extends SiteSettingChildrenHandler interface to support site header text updates.
Angular Site Settings & User State
editor/src/app/sites/settings/site-settings.state.ts, editor/src/app/sites/sections/site-sections.module.ts, editor/src/app/user/user.state.ts
Adds siteTexts case to settings dispatcher. Replaces SafePipe with PipesModule in SiteSectionsModule. Dispatches ClearAiChatAction during user logout cleanup.
Frontend Dependencies & Tooling
editor/package.json, .github/workflows/tests.yml, _api_app/.gitignore
Adds marked ^17.0.4 to Angular dependencies. Updates CI workflow PHP/Node versions and quote formatting. Extends .gitignore to ignore MCP/config JSON and TOML files.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant UI as AI Chat UI<br/>(Component)
    participant Store as NGXS Store<br/>(State)
    participant Service as AI Assistant<br/>Service
    participant API as Backend API<br/>(StateController)
    participant Agent as AssistantAgent<br/>(Laravel)

    User->>UI: Click AI icon / Open chat
    UI->>Store: Dispatch ToggleAiAssistantAction
    Store->>Store: Toggle isOpen flag

    User->>UI: Type message + Send
    UI->>Store: Dispatch SendAiMessageAction
    Store->>Store: Append user message, set isLoading
    
    Store->>Service: chat(message, history,<br/>site, template, changeHistory)
    Service->>API: POST /ai-chat with payload
    
    API->>Agent: Process message via<br/>AssistantAgent::fake/real
    Agent->>Agent: Generate reply with<br/>design_changes &<br/>settings_changes
    Agent-->>API: Return structured response
    
    API-->>Service: Return AiChatResponse
    Service-->>Store: Observable<AiChatResponse>
    
    Store->>Store: Dispatch AiMessageReceivedAction
    Store->>Store: Append assistant reply
    
    alt If design_changes exist
        Store->>Store: Dispatch UpdateSiteTemplateSettingsAction
    end
    
    alt If settings_changes exist
        Store->>Store: Dispatch UpdateSiteSettingsAction
    end
    
    Store->>Store: Update changeHistory for undo support
    Store->>Store: Set isLoading = false
    
    Store-->>UI: Update observables (messages, isLoading)
    UI->>UI: Auto-scroll to bottom
    UI-->>User: Display assistant reply & applied changes

    User->>UI: Click Clear button
    UI->>Store: Dispatch ClearAiChatAction
    Store->>Store: Reset to defaults (empty messages,<br/>isOpen: false, etc.)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through Laravels so grand,
With Anthropic whispers at its command,
NGXS flows like carrots through state,
Markdown tales and siteTexts await,
The AI assistant hops—chat is first-rate! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Ai assistant widget' accurately summarizes the main change: a new AI assistant widget/component feature added across frontend and backend, including new actions, state management, services, and UI components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ai-assistant
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
_api_app/app/Http/Middleware/SetupMiddleware.php (2)

26-50: ⚠️ Potential issue | 🟡 Minor

Add missing return type and curly braces for control structures.

The method violates coding guidelines:

  1. Missing explicit return type declaration (should be : void)
  2. Control structures on lines 33-39 lack curly braces

As per coding guidelines: "Always use curly braces for control structures, even for single-line statements" and "Always use explicit return type declarations for methods and functions."

📝 Proposed fix
-    protected function updateEnvFile()
+    protected function updateEnvFile(): void
     {
         $env_example = $this->parseEnvFile(base_path() . '/.env.example');
         $env = $this->parseEnvFile(base_path() . '/.env');
 
         $env_new = [];
         foreach ($env_example as $key => $value) {
-            if (isset($env[$key])) {
+            if (isset($env[$key])) {
                 $env_new[$key] = $env[$key];
-            } elseif ($key == 'APP_KEY' || $key == 'APP_ID') {
+            } elseif ($key == 'APP_KEY' || $key == 'APP_ID') {
                 $env_new[$key] = Helpers::uuid_v4();
-            } else {
+            } else {
                 $env_new[$key] = $value;
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@_api_app/app/Http/Middleware/SetupMiddleware.php` around lines 26 - 50, The
updateEnvFile method is missing an explicit return type and uses control
structures without braces; update the method signature to include a void return
type (function updateEnvFile(): void) and add curly braces for the
if/elseif/else blocks inside the loop that builds $env_new (the checks using
isset($env[$key]), $key == 'APP_KEY' || $key == 'APP_ID', and the final else),
ensuring each branch has braces, and keep the rest of the logic (parseEnvFile
calls, Helpers::uuid_v4(), building $content and file_put_contents) unchanged.

52-74: ⚠️ Potential issue | 🟡 Minor

Add missing type declarations and curly braces for control structures.

The method violates coding guidelines:

  1. Missing explicit return type declaration (should be : array)
  2. Missing parameter type hint for $file (should be string)
  3. Control structures on lines 56-58 and 62-64 lack curly braces

As per coding guidelines: "Always use curly braces for control structures, even for single-line statements", "Always use explicit return type declarations for methods and functions", and "Use appropriate PHP type hints for method parameters."

📝 Proposed fix
-    protected function parseEnvFile($file)
+    protected function parseEnvFile(string $file): array
     {
         $res = [];
 
-        if (! file_exists($file)) {
+        if (! file_exists($file)) {
             return $res;
+        }
 
         $content = trim(file_get_contents(realpath($file)));
 
-        if (empty($content)) {
+        if (empty($content)) {
             return $res;
+        }
 
         $rows = preg_split('/\s+/', $content);
         // Loop through given data
         foreach ((array) $rows as $key => $value) {
             [$key, $value] = explode('=', $value, 2);
             $res[$key] = $value;
         }
 
         return $res;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@_api_app/app/Http/Middleware/SetupMiddleware.php` around lines 52 - 74, The
parseEnvFile method lacks a parameter type and explicit return type and uses
single-line control structures without braces; update the function signature
parseEnvFile($file) to parseEnvFile(string $file): array, add curly braces for
the if (! file_exists($file)) { ... }, if (empty($content)) { ... } and the
foreach loop body foreach ((array) $rows as $key => $value) { ... }, and ensure
the method still returns the $res array at the end.
🧹 Nitpick comments (8)
_api_app/app/Http/Controllers/StateController.php (1)

136-147: LGTM!

Adding the explicit JsonResponse return type aligns with coding guidelines and improves type safety.

For consistency, consider adding return type declarations to other methods in this controller (get(), getMeta(), getSentryDSN()) in a follow-up PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@_api_app/app/Http/Controllers/StateController.php` around lines 136 - 147,
The getLocaleSettings method now has an explicit JsonResponse return type; apply
the same consistency to the other controller methods by adding appropriate
return type declarations: update get(), getMeta(), and getSentryDSN() to declare
their return types (e.g., JsonResponse or Response as appropriate for each
method's actual return value) so signatures match getLocaleSettings and improve
type safety and consistency across the controller.
_api_app/app/Http/Middleware/SetupMiddleware.php (1)

66-71: Consider using a robust .env parser library.

The current parsing logic splits on whitespace and may fail for:

  • Multi-line values
  • Values containing spaces
  • Malformed lines without = separator

Consider using a well-tested library like vlucas/phpdotenv (already a Laravel dependency) or Laravel's config layer instead of custom parsing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@_api_app/app/Http/Middleware/SetupMiddleware.php` around lines 66 - 71,
Replace the fragile custom parser in SetupMiddleware (the preg_split on
$content, the foreach over $rows and the explode('=',$value,2) that populates
$res) with a robust .env parsing approach: use vlucas/phpdotenv (or Laravel's
built-in env/config loader) to parse $content into key/value pairs and populate
the $res array, handling multi-line values, spaces and malformed lines safely;
update the code to call Dotenv or the appropriate loader API instead of manually
splitting by whitespace and exploding on '=' and remove the fragile foreach
parsing logic.
editor/src/app/ai-assistant/ai-assistant.state.ts (2)

175-189: Multiple dispatch calls could be batched for efficiency.

Each iteration dispatches a separate action. While functionally correct, NGXS allows dispatching an array of actions in a single call for better performance.

♻️ Proposed refactor to batch dispatches
-    for (const change of action.designChanges) {
-      dispatch(
-        new UpdateSiteTemplateSettingsAction(change.group, {
-          [change.setting]: change.value,
-        }),
-      );
-    }
-
-    for (const change of action.settingsChanges) {
-      dispatch(
-        new UpdateSiteSettingsAction(change.group, {
-          [change.setting]: change.value,
-        }),
-      );
-    }
+    const actions = [
+      ...action.designChanges.map(
+        (change) =>
+          new UpdateSiteTemplateSettingsAction(change.group, {
+            [change.setting]: change.value,
+          }),
+      ),
+      ...action.settingsChanges.map(
+        (change) =>
+          new UpdateSiteSettingsAction(change.group, {
+            [change.setting]: change.value,
+          }),
+      ),
+    ];
+
+    if (actions.length > 0) {
+      dispatch(actions);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@editor/src/app/ai-assistant/ai-assistant.state.ts` around lines 175 - 189,
Currently each loop calls dispatch per change which is inefficient; instead map
action.designChanges to an array of new
UpdateSiteTemplateSettingsAction(change.group, {[change.setting]: change.value})
and map action.settingsChanges to an array of new
UpdateSiteSettingsAction(change.group, {[change.setting]: change.value}),
concatenate those two arrays (preserving order) and call dispatch once with the
combined array; locate the loops where action.designChanges and
action.settingsChanges are iterated and replace them with the single batched
dispatch call using the generated action instances.

127-131: Consider providing user-facing error feedback.

When the AI request fails, the error is logged to console and loading state is cleared, but the user receives no visible indication of what went wrong. Consider adding an error message to the messages array or dispatching a notification action.

💡 Proposed enhancement for error feedback
       catchError((error) => {
         console.error('AI assistant error:', error);
-        patchState({ isLoading: false });
+        const errorMessage: AiMessage = {
+          role: 'assistant',
+          content: 'Sorry, something went wrong. Please try again.',
+        };
+        patchState({
+          messages: [...getState().messages, errorMessage],
+          isLoading: false,
+        });
         return EMPTY;
       }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@editor/src/app/ai-assistant/ai-assistant.state.ts` around lines 127 - 131,
The catchError block in the AI assistant observable only logs and clears
isLoading, so add user-facing feedback by appending an error message to the
messages state or dispatching a notification action: inside the catchError in
ai-assistant.state.ts update patchState to both set isLoading: false and push a
new message object into the messages array (e.g., a system/error message with a
clear text like "AI request failed") or call your notification action (e.g.,
dispatch(NotificationActions.showError(...))). Ensure you reference the same
patchState and messages state shape used elsewhere so the UI displays the error
to the user.
editor/src/app/ai-assistant/ai-assistant.actions.ts (1)

10-18: Consider reusing AiChangeItem interface to reduce duplication.

The inline type { group: string; setting: string; value: string; previous_value?: string | null }[] duplicates the AiChangeItem interface defined in ai-assistant.service.ts. Reusing the interface improves maintainability.

♻️ Proposed refactor to reuse interface
+import { AiChangeItem } from './ai-assistant.service';
+
 export class AiMessageReceivedAction {
   static readonly type = 'AI_ASSISTANT:MESSAGE_RECEIVED';
   constructor(
     public reply: string,
-    public designChanges: { group: string; setting: string; value: string; previous_value?: string | null }[],
-    public settingsChanges: { group: string; setting: string; value: string; previous_value?: string | null }[],
+    public designChanges: AiChangeItem[],
+    public settingsChanges: AiChangeItem[],
     public isUndo: boolean = false,
   ) {}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@editor/src/app/ai-assistant/ai-assistant.actions.ts` around lines 10 - 18,
The inline change item type in AiMessageReceivedAction duplicates the
AiChangeItem interface; update the constructor signatures for designChanges and
settingsChanges to use AiChangeItem[] instead of the inline type and add an
import for AiChangeItem where it’s defined (so AiMessageReceivedAction
references the shared interface), ensuring any nullable previous_value semantics
match AiChangeItem.
editor/src/app/rerender/template-rerender.service.ts (1)

96-99: Minor inconsistency: null check not used for other cases.

The if (info.siteTexts) guard is present here but not for other cases like BANNERS_SETTINGS, SETTINGS, or ENTRY_LAYOUT (lines 84-92). Since siteTexts is a required field in SiteSettingChildrenHandler (per context snippet 1), this check is redundant. However, if this is intentional defensive coding for a newly added field, it's acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@editor/src/app/rerender/template-rerender.service.ts` around lines 96 - 99,
The SITE_TEXTS branch currently has an extra conditional guard (if
(info.siteTexts)) while other branches like BANNERS_SETTINGS, SETTINGS, and
ENTRY_LAYOUT directly push their fields; to keep consistency remove the
redundant null-check and change the SITE_TEXTS case to simply push
info.siteTexts into compList (i.e., compList.push(info.siteTexts);) so it
matches the pattern used in the other cases and relies on the type contract of
SiteSettingChildrenHandler.
_api_app/.env.example (1)

9-10: Consider alphabetically ordering environment variables.

The static analysis tool suggests placing AI_DEFAULT_PROVIDER and ANTHROPIC_API_KEY before API_PREFIX for alphabetical consistency. This is a minor style consideration that improves maintainability as the file grows.

♻️ Suggested ordering
 APP_ENV=production
 APP_DEBUG=false
+AI_DEFAULT_PROVIDER=anthropic
+ANTHROPIC_API_KEY=
 CACHE_STORE=file
 APP_KEY=[YOUR_APP_KEY]
 APP_ID=[YOUR_APP_ID]
 API_PREFIX=_api
 SENTRY_DSN=
 SENTRY_FRONTEND_DSN=
-AI_DEFAULT_PROVIDER=anthropic
-ANTHROPIC_API_KEY=
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@_api_app/.env.example` around lines 9 - 10, Reorder the environment variable
declarations so they are alphabetized: move AI_DEFAULT_PROVIDER and
ANTHROPIC_API_KEY so they appear before API_PREFIX in .env.example; ensure the
variables (AI_DEFAULT_PROVIDER, ANTHROPIC_API_KEY, API_PREFIX) are in ascending
alphabetical order and keep the rest of the file formatting unchanged.
_api_app/.github/skills/ai-sdk-development/SKILL.md (1)

1-413: Consider a single-source approach for duplicated skill docs.

This content is duplicated across .github/.cursor/.agents variants, which will be hard to keep in sync. A template/generation step would reduce drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@_api_app/.github/skills/ai-sdk-development/SKILL.md` around lines 1 - 413,
The ai-sdk-development SKILL.md is duplicated across multiple variants;
consolidate into a single source (e.g., a template or canonical SKILL.md for the
ai-sdk-development skill) and update the duplicated copies to be generated or
included from that canonical file; modify the generator/script (or add a small
build step/CI job) to produce the variant files from the canonical
ai-sdk-development SKILL.md (or SKILL.md.template) and update references to the
skill name "ai-sdk-development" so all variants pull from the single source.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@_api_app/.codex/config.toml`:
- Around line 1-12: The config contains hardcoded absolute paths (cwd and
SITE_PATH in the mcp_servers sections: mcp_servers.laravel-boost,
mcp_servers.herd, and mcp_servers.herd.env) which break for other developers;
replace those absolute paths with relative paths or environment-variable
references (e.g., use "./_api_app" or a variable like ${SITE_PATH}) or remove
the file from VCS and add a template (e.g., config.toml.example) and add this
file to .gitignore so each developer can configure their own local cwd and
SITE_PATH values.

In `@_api_app/AGENTS.md`:
- Line 248: Replace the incorrect skill name reference "developing-with-ai-sdk"
with the defined skill name "ai-sdk-development" so activation uses the
canonical identifier; update any occurrence in the AGENTS.md text (the line that
currently reads Activate the `developing-with-ai-sdk` skill) to read Activate
the `ai-sdk-development` skill to ensure the skill activates correctly.

In `@_api_app/CLAUDE.md`:
- Around line 248-249: The skill name string in the CLAUDE.md content is
incorrect: replace the lone occurrence of "developing-with-ai-sdk" with the
canonical skill id "ai-sdk-development" to match boost.json and the SKILL.md
frontmatter; verify any other occurrences in this file use "ai-sdk-development"
so the skill activation string in the documentation matches the
boost.json/SKILL.md definition.

In `@_api_app/opencode.json`:
- Around line 20-22: The SITE_PATH in the "environment" object is a hardcoded,
user-specific absolute path; replace it with a portable solution by: change
SITE_PATH to use a relative path or an environment variable reference (e.g. read
from process env at runtime) and remove or template the absolute value in
opencode.json so it no longer contains "/Users/uldis/..."; alternatively, mark
this file as developer-specific by adding it to .gitignore or provide a sample
file (e.g. opencode.example.json) with a placeholder for SITE_PATH so other
developers/CI can set their own path.

In `@_api_app/tests/Feature/AiChatControllerTest.php`:
- Around line 10-16: The test "returns 401 when unauthenticated" uses
assertStatus(401) — change this to the more specific assertion by replacing
assertStatus(401) with assertUnauthorized() on the response from
post(route('ai_chat'), [...]) so the test uses the preferred Laravel assertion;
locate the anonymous test function (it('returns 401 when unauthenticated', ...))
and update the assertion accordingly.

In `@editor/src/app/ai-assistant/ai-assistant.component.ts`:
- Around line 40-51: The template is inserting rendered HTML via
[innerHTML]="msg.content | markdown" which relies on the markdown pipe to bypass
Angular XSS protections; update the markdown.pipe (used by
ai-assistant.component template) to treat AI-generated content as untrusted:
parse markdown to HTML, run the HTML through a robust sanitizer (e.g.,
DOMPurify) inside the MarkdownPipe, and only then, if necessary, use
DomSanitizer.bypassSecurityTrustHtml on the already-sanitized output; ensure the
pipe is named MarkdownPipe (or the exported pipe symbol used in the template)
and that ai-assistant.component continues to consume messages$ / msg.content
without changing template bindings.

In `@editor/src/app/pipes/markdown.pipe.ts`:
- Line 9: The transform method should guard against null/undefined input before
calling marked.parse; update transform(value: string): SafeHtml in the markdown
pipe to return a safe empty string or a default SafeHtml (e.g.,
sanitizer.bypassSecurityTrustHtml('')) when value is null/undefined, and only
call marked.parse(value) when value is a non-empty string; ensure the return
type SafeHtml is preserved and reference marked.parse and transform to locate
the change.
- Around line 9-17: The transform method in markdown.pipe.ts uses marked.parse
with a custom Renderer and then calls this.sanitizer.bypassSecurityTrustHtml
without sanitizing or escaping attributes, which allows raw HTML, script tags
and attribute injection (href, title, text) — fix by installing and importing
DOMPurify, run DOMPurify.sanitize on the string returned by marked.parse (pass a
safe ALLOWED_TAGS/ALLOWED_ATTRS config and allow target/rel if needed), stop
interpolating raw attribute values by ensuring marked's renderer produces safe
attributes (or escape href/title/text before constructing the <a>), and only
call this.sanitizer.bypassSecurityTrustHtml on the sanitized output; keep
references to transform, Renderer, marked.parse and
this.sanitizer.bypassSecurityTrustHtml when making the change.

---

Outside diff comments:
In `@_api_app/app/Http/Middleware/SetupMiddleware.php`:
- Around line 26-50: The updateEnvFile method is missing an explicit return type
and uses control structures without braces; update the method signature to
include a void return type (function updateEnvFile(): void) and add curly braces
for the if/elseif/else blocks inside the loop that builds $env_new (the checks
using isset($env[$key]), $key == 'APP_KEY' || $key == 'APP_ID', and the final
else), ensuring each branch has braces, and keep the rest of the logic
(parseEnvFile calls, Helpers::uuid_v4(), building $content and
file_put_contents) unchanged.
- Around line 52-74: The parseEnvFile method lacks a parameter type and explicit
return type and uses single-line control structures without braces; update the
function signature parseEnvFile($file) to parseEnvFile(string $file): array, add
curly braces for the if (! file_exists($file)) { ... }, if (empty($content)) {
... } and the foreach loop body foreach ((array) $rows as $key => $value) { ...
}, and ensure the method still returns the $res array at the end.

---

Nitpick comments:
In `@_api_app/.env.example`:
- Around line 9-10: Reorder the environment variable declarations so they are
alphabetized: move AI_DEFAULT_PROVIDER and ANTHROPIC_API_KEY so they appear
before API_PREFIX in .env.example; ensure the variables (AI_DEFAULT_PROVIDER,
ANTHROPIC_API_KEY, API_PREFIX) are in ascending alphabetical order and keep the
rest of the file formatting unchanged.

In `@_api_app/.github/skills/ai-sdk-development/SKILL.md`:
- Around line 1-413: The ai-sdk-development SKILL.md is duplicated across
multiple variants; consolidate into a single source (e.g., a template or
canonical SKILL.md for the ai-sdk-development skill) and update the duplicated
copies to be generated or included from that canonical file; modify the
generator/script (or add a small build step/CI job) to produce the variant files
from the canonical ai-sdk-development SKILL.md (or SKILL.md.template) and update
references to the skill name "ai-sdk-development" so all variants pull from the
single source.

In `@_api_app/app/Http/Controllers/StateController.php`:
- Around line 136-147: The getLocaleSettings method now has an explicit
JsonResponse return type; apply the same consistency to the other controller
methods by adding appropriate return type declarations: update get(), getMeta(),
and getSentryDSN() to declare their return types (e.g., JsonResponse or Response
as appropriate for each method's actual return value) so signatures match
getLocaleSettings and improve type safety and consistency across the controller.

In `@_api_app/app/Http/Middleware/SetupMiddleware.php`:
- Around line 66-71: Replace the fragile custom parser in SetupMiddleware (the
preg_split on $content, the foreach over $rows and the explode('=',$value,2)
that populates $res) with a robust .env parsing approach: use vlucas/phpdotenv
(or Laravel's built-in env/config loader) to parse $content into key/value pairs
and populate the $res array, handling multi-line values, spaces and malformed
lines safely; update the code to call Dotenv or the appropriate loader API
instead of manually splitting by whitespace and exploding on '=' and remove the
fragile foreach parsing logic.

In `@editor/src/app/ai-assistant/ai-assistant.actions.ts`:
- Around line 10-18: The inline change item type in AiMessageReceivedAction
duplicates the AiChangeItem interface; update the constructor signatures for
designChanges and settingsChanges to use AiChangeItem[] instead of the inline
type and add an import for AiChangeItem where it’s defined (so
AiMessageReceivedAction references the shared interface), ensuring any nullable
previous_value semantics match AiChangeItem.

In `@editor/src/app/ai-assistant/ai-assistant.state.ts`:
- Around line 175-189: Currently each loop calls dispatch per change which is
inefficient; instead map action.designChanges to an array of new
UpdateSiteTemplateSettingsAction(change.group, {[change.setting]: change.value})
and map action.settingsChanges to an array of new
UpdateSiteSettingsAction(change.group, {[change.setting]: change.value}),
concatenate those two arrays (preserving order) and call dispatch once with the
combined array; locate the loops where action.designChanges and
action.settingsChanges are iterated and replace them with the single batched
dispatch call using the generated action instances.
- Around line 127-131: The catchError block in the AI assistant observable only
logs and clears isLoading, so add user-facing feedback by appending an error
message to the messages state or dispatching a notification action: inside the
catchError in ai-assistant.state.ts update patchState to both set isLoading:
false and push a new message object into the messages array (e.g., a
system/error message with a clear text like "AI request failed") or call your
notification action (e.g., dispatch(NotificationActions.showError(...))). Ensure
you reference the same patchState and messages state shape used elsewhere so the
UI displays the error to the user.

In `@editor/src/app/rerender/template-rerender.service.ts`:
- Around line 96-99: The SITE_TEXTS branch currently has an extra conditional
guard (if (info.siteTexts)) while other branches like BANNERS_SETTINGS,
SETTINGS, and ENTRY_LAYOUT directly push their fields; to keep consistency
remove the redundant null-check and change the SITE_TEXTS case to simply push
info.siteTexts into compList (i.e., compList.push(info.siteTexts);) so it
matches the pattern used in the other cases and relies on the type contract of
SiteSettingChildrenHandler.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4272ee19-9253-4285-827b-de9b8dcbc0b0

📥 Commits

Reviewing files that changed from the base of the PR and between 2b80b71 and 633f097.

⛔ Files ignored due to path filters (2)
  • _api_app/composer.lock is excluded by !**/*.lock
  • editor/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (50)
  • CLAUDE.md
  • WARP.md
  • _api_app/.agents/skills/ai-sdk-development/SKILL.md
  • _api_app/.agents/skills/pest-testing/SKILL.md
  • _api_app/.claude/skills/ai-sdk-development/SKILL.md
  • _api_app/.claude/skills/pest-testing/SKILL.md
  • _api_app/.codex/config.toml
  • _api_app/.cursor/skills/ai-sdk-development/SKILL.md
  • _api_app/.cursor/skills/pest-testing/SKILL.md
  • _api_app/.env.example
  • _api_app/.github/skills/ai-sdk-development/SKILL.md
  • _api_app/.github/skills/pest-testing/SKILL.md
  • _api_app/AGENTS.md
  • _api_app/CLAUDE.md
  • _api_app/app/Http/Controllers/StateController.php
  • _api_app/app/Http/Middleware/Authenticate.php
  • _api_app/app/Http/Middleware/SetupMiddleware.php
  • _api_app/app/User/UserModel.php
  • _api_app/boost.json
  • _api_app/bootstrap/providers.php
  • _api_app/composer.json
  • _api_app/config/ai.php
  • _api_app/config/auth.php
  • _api_app/config/sanctum.php
  • _api_app/config/twigbridge.php
  • _api_app/database/factories/UserFactory.php
  • _api_app/opencode.json
  • _api_app/tests/Feature/AiChatControllerTest.php
  • _api_app/tests/Pest.php
  • editor/package.json
  • editor/src/app/ai-assistant/ai-assistant.actions.ts
  • editor/src/app/ai-assistant/ai-assistant.component.ts
  • editor/src/app/ai-assistant/ai-assistant.module.ts
  • editor/src/app/ai-assistant/ai-assistant.service.ts
  • editor/src/app/ai-assistant/ai-assistant.state.ts
  • editor/src/app/app.component.ts
  • editor/src/app/app.module.ts
  • editor/src/app/header/header.component.ts
  • editor/src/app/pipes/markdown.pipe.ts
  • editor/src/app/pipes/pipes.module.ts
  • editor/src/app/pipes/safe.pipe.ts
  • editor/src/app/rerender/default-template-rerender.service.ts
  • editor/src/app/rerender/mashup/mashup-template-rerender.service.ts
  • editor/src/app/rerender/messy/messy-template-rerender.service.ts
  • editor/src/app/rerender/template-rerender.service.ts
  • editor/src/app/rerender/types/components.ts
  • editor/src/app/rerender/white/white-template-rerender.service.ts
  • editor/src/app/sites/sections/site-sections.module.ts
  • editor/src/app/sites/settings/site-settings.state.ts
  • editor/src/app/user/user.state.ts
💤 Files with no reviewable changes (2)
  • WARP.md
  • _api_app/app/Http/Middleware/Authenticate.php

Comment on lines +1 to +12
[mcp_servers.laravel-boost]
command = "php"
args = ["artisan", "boost:mcp"]
cwd = "/Users/uldis/projects/berta/berta/_api_app"

[mcp_servers.herd]
command = "php"
args = ["/Applications/Herd.app/Contents/Resources/herd-mcp.phar"]
cwd = "/Users/uldis/projects/berta/berta/_api_app"

[mcp_servers.herd.env]
SITE_PATH = "/Users/uldis/projects/berta/berta/_api_app"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Hardcoded absolute paths will break for other developers.

The configuration contains machine-specific paths (/Users/uldis/projects/berta/berta/_api_app) that won't work for other team members. Consider using relative paths or removing this file from version control if it's meant for local development only.

🔧 Suggested fix using relative paths
 [mcp_servers.laravel-boost]
 command = "php"
 args = ["artisan", "boost:mcp"]
-cwd = "/Users/uldis/projects/berta/berta/_api_app"
+cwd = "."

 [mcp_servers.herd]
 command = "php"
 args = ["/Applications/Herd.app/Contents/Resources/herd-mcp.phar"]
-cwd = "/Users/uldis/projects/berta/berta/_api_app"
+cwd = "."

 [mcp_servers.herd.env]
-SITE_PATH = "/Users/uldis/projects/berta/berta/_api_app"
+SITE_PATH = "."

Alternatively, add this file to .gitignore and provide a .codex/config.toml.example template instead.

📝 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
[mcp_servers.laravel-boost]
command = "php"
args = ["artisan", "boost:mcp"]
cwd = "/Users/uldis/projects/berta/berta/_api_app"
[mcp_servers.herd]
command = "php"
args = ["/Applications/Herd.app/Contents/Resources/herd-mcp.phar"]
cwd = "/Users/uldis/projects/berta/berta/_api_app"
[mcp_servers.herd.env]
SITE_PATH = "/Users/uldis/projects/berta/berta/_api_app"
[mcp_servers.laravel-boost]
command = "php"
args = ["artisan", "boost:mcp"]
cwd = "."
[mcp_servers.herd]
command = "php"
args = ["/Applications/Herd.app/Contents/Resources/herd-mcp.phar"]
cwd = "."
[mcp_servers.herd.env]
SITE_PATH = "."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@_api_app/.codex/config.toml` around lines 1 - 12, The config contains
hardcoded absolute paths (cwd and SITE_PATH in the mcp_servers sections:
mcp_servers.laravel-boost, mcp_servers.herd, and mcp_servers.herd.env) which
break for other developers; replace those absolute paths with relative paths or
environment-variable references (e.g., use "./_api_app" or a variable like
${SITE_PATH}) or remove the file from VCS and add a template (e.g.,
config.toml.example) and add this file to .gitignore so each developer can
configure their own local cwd and SITE_PATH values.


## Test Enforcement
- This application uses the Laravel AI SDK (`laravel/ai`) for all AI functionality.
- Activate the `developing-with-ai-sdk` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter).
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix incorrect AI skill name reference.

Line 248 references developing-with-ai-sdk, but the defined skill name is ai-sdk-development (see Line 29). This mismatch can prevent correct skill activation.

🛠️ Suggested fix
-- Activate the `developing-with-ai-sdk` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter).
+- Activate the `ai-sdk-development` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter).
📝 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
- Activate the `developing-with-ai-sdk` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter).
- Activate the `ai-sdk-development` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@_api_app/AGENTS.md` at line 248, Replace the incorrect skill name reference
"developing-with-ai-sdk" with the defined skill name "ai-sdk-development" so
activation uses the canonical identifier; update any occurrence in the AGENTS.md
text (the line that currently reads Activate the `developing-with-ai-sdk` skill)
to read Activate the `ai-sdk-development` skill to ensure the skill activates
correctly.

Comment on lines +248 to 249
- Activate the `developing-with-ai-sdk` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter).

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Skill name mismatch — should be ai-sdk-development.

The skill name here is developing-with-ai-sdk, but in boost.json and the SKILL.md frontmatter, it's defined as ai-sdk-development. This inconsistency may prevent proper skill activation.

Proposed fix
 ## Laravel AI SDK
 
 - This application uses the Laravel AI SDK (`laravel/ai`) for all AI functionality.
-- Activate the `developing-with-ai-sdk` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter).
+- Activate the `ai-sdk-development` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter).
📝 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
- Activate the `developing-with-ai-sdk` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter).
- Activate the `ai-sdk-development` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@_api_app/CLAUDE.md` around lines 248 - 249, The skill name string in the
CLAUDE.md content is incorrect: replace the lone occurrence of
"developing-with-ai-sdk" with the canonical skill id "ai-sdk-development" to
match boost.json and the SKILL.md frontmatter; verify any other occurrences in
this file use "ai-sdk-development" so the skill activation string in the
documentation matches the boost.json/SKILL.md definition.

Comment on lines +10 to +16
it('returns 401 when unauthenticated', function () {
post(route('ai_chat'), [
'message' => 'Make the background blue',
'site' => '',
'template' => 'default',
])->assertStatus(401);
})->skip(! $pluginInstalled, 'AiAssistant plugin not installed');
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use assertUnauthorized() instead of assertStatus(401).

As per coding guidelines, prefer specific response assertion methods over generic assertStatus().

Proposed fix
 it('returns 401 when unauthenticated', function () {
     post(route('ai_chat'), [
         'message' => 'Make the background blue',
         'site' => '',
         'template' => 'default',
-    ])->assertStatus(401);
+    ])->assertUnauthorized();
 })->skip(! $pluginInstalled, 'AiAssistant plugin not installed');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@_api_app/tests/Feature/AiChatControllerTest.php` around lines 10 - 16, The
test "returns 401 when unauthenticated" uses assertStatus(401) — change this to
the more specific assertion by replacing assertStatus(401) with
assertUnauthorized() on the response from post(route('ai_chat'), [...]) so the
test uses the preferred Laravel assertion; locate the anonymous test function
(it('returns 401 when unauthenticated', ...)) and update the assertion
accordingly.

Comment on lines +40 to +51
@for (msg of messages$ | async; track $index) {
<div
class="ai-message"
[class.ai-message--user]="msg.role === 'user'"
[class.ai-message--assistant]="msg.role === 'assistant'"
>
@if (msg.role === 'assistant') {
<span [innerHTML]="msg.content | markdown"></span>
} @else {
{{ msg.content }}
}
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Note: This template relies on the markdown pipe which has security concerns.

The markdown pipe used on line 47 bypasses Angular's XSS protection without proper sanitization. Please address the security issues flagged in the markdown.pipe.ts review before shipping this feature. AI-generated content should be treated as untrusted input.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@editor/src/app/ai-assistant/ai-assistant.component.ts` around lines 40 - 51,
The template is inserting rendered HTML via [innerHTML]="msg.content | markdown"
which relies on the markdown pipe to bypass Angular XSS protections; update the
markdown.pipe (used by ai-assistant.component template) to treat AI-generated
content as untrusted: parse markdown to HTML, run the HTML through a robust
sanitizer (e.g., DOMPurify) inside the MarkdownPipe, and only then, if
necessary, use DomSanitizer.bypassSecurityTrustHtml on the already-sanitized
output; ensure the pipe is named MarkdownPipe (or the exported pipe symbol used
in the template) and that ai-assistant.component continues to consume messages$
/ msg.content without changing template bindings.

export class MarkdownPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}

transform(value: string): SafeHtml {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add null/undefined guard for input value.

If value is null or undefined, marked.parse() may throw or produce unexpected output.

🛡️ Proposed fix
   transform(value: string): SafeHtml {
+    if (!value) {
+      return '';
+    }
     const renderer = new Renderer();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@editor/src/app/pipes/markdown.pipe.ts` at line 9, The transform method should
guard against null/undefined input before calling marked.parse; update
transform(value: string): SafeHtml in the markdown pipe to return a safe empty
string or a default SafeHtml (e.g., sanitizer.bypassSecurityTrustHtml('')) when
value is null/undefined, and only call marked.parse(value) when value is a
non-empty string; ensure the return type SafeHtml is preserved and reference
marked.parse and transform to locate the change.

Comment on lines +9 to +17
transform(value: string): SafeHtml {
const renderer = new Renderer();
renderer.link = ({ href, title, text }: { href: string; title?: string | null; text: string }) => {
const titleAttr = title ? ` title="${title}"` : '';
return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
};
const html = marked.parse(value, { renderer }) as string;
return this.sanitizer.bypassSecurityTrustHtml(html);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

How to safely sanitize HTML with DOMPurify in Angular applications?

💡 Result:

To safely sanitize HTML with DOMPurify in Angular applications, follow these steps for XSS protection when rendering user-generated content via [innerHTML]: 1. Install DOMPurify: npm install dompurify @types/dompurify 2. For basic manual sanitization in a component (recommended for control): import { Component } from '@angular/core'; import DOMPurify from 'dompurify'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @Component({ template: <div [innerHTML]="safeHtml"> }) export class MyComponent { rawHtml = Safe content; safeHtml: SafeHtml; constructor(private sanitizer: DomSanitizer) { const clean = DOMPurify.sanitize(this.rawHtml); this.safeHtml = this.sanitizer.bypassSecurityTrustHtml(clean); } } DOMPurify removes malicious attributes/scripts; DomSanitizer then marks it trusted for Angular. 3. For easier integration, use @taiga-ui/dompurify (latest v5.0.1 as of 2025, Angular 19+ compatible): npm install @taiga-ui/dompurify - As pipe (simplest): import { NgDompurifyModule } from '@taiga-ui/dompurify'; @NgModule({ imports: [NgDompurifyModule] }) export class AppModule {} <div [innerHtml]="rawHtml | dompurify"> - As service: constructor(private sanitizer: NgDompurifySanitizer) {} safeHtml = this.sanitizer.sanitize(SecurityContext.HTML, rawHtml); - Global replacement: providers: [ { provide: Sanitizer, useClass: NgDompurifySanitizer } ] 4. Configure DOMPurify (e.g., restrict tags): providers: [ { provide: DOMPURIFY_CONFIG, useValue: { ALLOWED_TAGS: ['b', 'i', 'p'] } } ] Best practices: - Always sanitize untrusted input at injection point. - Use restrictive whitelists for tags/attributes. - DOMPurify > Angular's basic sanitizer for complex HTML. - Test with XSS payloads; prefer pipes/services over manual for consistency. This prevents XSS while allowing safe HTML rendering.

Citations:


🏁 Script executed:

# Check if marked library is installed and what version
cd /tmp && npm list marked 2>/dev/null || cat package.json | grep -A2 '"marked"' || find . -name "package.json" -type f -exec grep -l "marked" {} \;

Repository: berta-cms/berta

Length of output: 138


🏁 Script executed:

# Find the markdown.pipe.ts file to verify it exists and matches the snippet
fd -t f "markdown.pipe.ts"

Repository: berta-cms/berta

Length of output: 96


🏁 Script executed:

# Check if DOMPurify is already installed or referenced
cd /tmp && npm list dompurify 2>/dev/null || find . -name "package.json" -type f -exec grep -l "dompurify\|DOMPurify" {} \;

Repository: berta-cms/berta

Length of output: 76


🏁 Script executed:

# Search for marked library usage and configuration in the codebase
rg "marked\|Renderer" --type ts --type js -A 3 -B 1

Repository: berta-cms/berta

Length of output: 41


🏁 Script executed:

# Read the markdown.pipe.ts file
cat editor/src/app/pipes/markdown.pipe.ts

Repository: berta-cms/berta

Length of output: 844


🏁 Script executed:

# Find package.json to check dependencies
find . -maxdepth 2 -name "package.json" -type f | head -5

Repository: berta-cms/berta

Length of output: 119


🏁 Script executed:

# Look for marked or sanitizer usage in the entire codebase
rg "marked|DomSanitizer|sanitize" --type ts --type js | head -30

Repository: berta-cms/berta

Length of output: 3184


🏁 Script executed:

# Check the main package.json for marked version
cat package.json | grep -A 2 -B 2 "marked"

Repository: berta-cms/berta

Length of output: 41


🏁 Script executed:

# Check editor package.json
cat editor/package.json | grep -A 2 -B 2 "marked"

Repository: berta-cms/berta

Length of output: 209


🏁 Script executed:

# Search for how this markdown pipe is being used in the codebase
rg "markdown\s*['\"]|MarkdownPipe" --type ts --type html -B 2 -A 2

Repository: berta-cms/berta

Length of output: 1529


🏁 Script executed:

# Check the marked v17 documentation and behavior regarding HTML escaping
# First, let's see if there's any escape/sanitization happening
rg "escape|sanitize|xss|Security" editor/src/app/pipes/ --type ts

Repository: berta-cms/berta

Length of output: 990


🏁 Script executed:

# Check the ai-assistant component to understand context better
cat editor/src/app/ai-assistant/ai-assistant.component.ts | head -100

Repository: berta-cms/berta

Length of output: 2964


🏁 Script executed:

# Check what marked v17 does regarding HTML sanitization by default
# Let's look at the marked package documentation or check if there are any config options
cd editor && npm ls marked 2>/dev/null | head -10

Repository: berta-cms/berta

Length of output: 109


🏁 Script executed:

# Create a test to understand marked v17 behavior with HTML input
python3 << 'EOF'
import subprocess
import json

# Check if we can inspect the marked module or its documentation
result = subprocess.run(
    ["npm", "info", "marked@17.0.4", "--json"],
    cwd="editor",
    capture_output=True,
    text=True,
    timeout=10
)

if result.returncode == 0:
    try:
        info = json.loads(result.stdout)
        # Look for security-related info
        if "description" in info:
            print("Description:", info["description"][:500])
        if "repository" in info:
            print("Repository:", info.get("repository", {}).get("url", "N/A"))
    except:
        print(result.stdout[:1000])
else:
    print("Error:", result.stderr[:500])
EOF

Repository: berta-cms/berta

Length of output: 154


🏁 Script executed:

# Verify marked v17 behavior with HTML by checking its actual code or docs
# Let's look for any existing security considerations or issues
python3 << 'EOF'
# Test to understand if marked v17 sanitizes by default
# marked v17 should NOT sanitize HTML by default - this is documented behavior
# The library intentionally does not sanitize to allow users to choose their sanitizer

import subprocess

# Get information about marked's current behavior
result = subprocess.run(
    ["npm", "view", "marked@17.0.4", "readme"],
    cwd="editor",
    capture_output=True,
    text=True,
    timeout=10
)

# Print a snippet to verify
if result.returncode == 0 and result.stdout:
    lines = result.stdout.split('\n')
    # Look for security or sanitization mentions
    for i, line in enumerate(lines):
        if any(keyword in line.lower() for keyword in ['sanitize', 'xss', 'security', 'escape', 'html']):
            start = max(0, i - 2)
            end = min(len(lines), i + 3)
            print('\n'.join(lines[start:end]))
            print("---")
else:
    print("Could not fetch readme")
EOF

Repository: berta-cms/berta

Length of output: 1484


🏁 Script executed:

# Let's verify the vulnerability by checking if href/title values could contain XSS payloads
# In marked, are href/title/text values escaped or raw?
python3 << 'EOF'
# Check documentation claim: marked v17 does NOT escape HTML by default
# This is intentional - users must handle sanitization themselves

# Key facts about marked v17:
# 1. Does NOT sanitize HTML by default
# 2. Passes through raw content
# 3. Does NOT escape attribute values
# 4. Users are responsible for sanitization

print("marked v17 key facts:")
print("1. HTML sanitization: DISABLED by default")
print("2. Attribute escaping: NOT APPLIED")
print("3. Responsibility: User must sanitize")
print("\nXSS Attack Vectors in current code:")
print("- href='javascript:void(0)' -> executable")
print("- title='x\" onclick=\"alert()\"' -> attribute breakout")  
print("- text with <script> tags -> raw HTML injection")
EOF

Repository: berta-cms/berta

Length of output: 375


🏁 Script executed:

# Check if there's a better way to handle the security attributes in marked v17
# Look for any marked configuration options for link attributes
python3 << 'EOF'
# In marked v17, after sanitization with DOMPurify,
# we lose the target="_blank" and rel="noopener noreferrer" if we remove the renderer

# Options:
# 1. Keep custom renderer but escape the values
# 2. Use marked's renderer + DOMPurify config to allow target attribute
# 3. Post-process DOMPurify output to add attributes

# The correct approach would be to ESCAPE the values, not remove the renderer

print("Correct fix approaches:")
print("\n1. ESCAPE attribute values (simpler):")
print("   Use escapeHtml() on href, title, text before interpolation")
print("\n2. SANITIZE full output (proposed diff):")
print("   Use DOMPurify on full marked output")
print("   Then handle target/rel attributes via config or post-processing")
print("\nThe review's proposed diff is incomplete:")
print("- Removes renderer entirely (loses security attributes)")
print("- Doesn't show how to re-add target/rel")
print("- DOMPurify config shown doesn't handle target attribute setup")
EOF

Repository: berta-cms/berta

Length of output: 533


Critical: XSS vulnerability – marked does not sanitize HTML by default.

The marked library (v17.0.4) explicitly does not sanitize HTML and documents this as the user's responsibility. The current code has three critical security issues:

  1. No HTML sanitization: marked.parse() passes through raw HTML, allowing <script> tags and event handlers from AI responses.

  2. Unescaped attribute values (lines 12–13): href, title, and text are interpolated directly into the HTML string without escaping, enabling:

    • href="javascript:alert('xss')"
    • title breaking out of the attribute: " onclick="malicious()"
    • text containing raw HTML tags
  3. bypassSecurityTrustHtml completely disables Angular's built-in XSS protection.

Since the pipe renders untrusted AI assistant responses, this is exploitable.

Fix: Use DOMPurify to sanitize the marked output before bypassing security:

import DOMPurify from 'dompurify';

const html = marked.parse(value, { renderer }) as string;
const sanitized = DOMPurify.sanitize(html, {
  ADD_ATTR: ['target', 'rel'],
  ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li', 'code', 'pre'],
});
return this.sanitizer.bypassSecurityTrustHtml(sanitized);

Install DOMPurify: npm install dompurify @types/dompurify``

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@editor/src/app/pipes/markdown.pipe.ts` around lines 9 - 17, The transform
method in markdown.pipe.ts uses marked.parse with a custom Renderer and then
calls this.sanitizer.bypassSecurityTrustHtml without sanitizing or escaping
attributes, which allows raw HTML, script tags and attribute injection (href,
title, text) — fix by installing and importing DOMPurify, run DOMPurify.sanitize
on the string returned by marked.parse (pass a safe ALLOWED_TAGS/ALLOWED_ATTRS
config and allow target/rel if needed), stop interpolating raw attribute values
by ensuring marked's renderer produces safe attributes (or escape
href/title/text before constructing the <a>), and only call
this.sanitizer.bypassSecurityTrustHtml on the sanitized output; keep references
to transform, Renderer, marked.parse and this.sanitizer.bypassSecurityTrustHtml
when making the change.

…aths

Four AI tool config files contained an absolute machine-specific path. They are now
gitignored and replaced with .example counterparts using /path/to/berta/_api_app.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant