diff --git a/azure.yaml b/azure.yaml
index 97cab5f3..ae800df5 100644
--- a/azure.yaml
+++ b/azure.yaml
@@ -25,7 +25,7 @@ hooks:
Write-Host " ---------------------------------------------------------------------------------------------------" -ForegroundColor Yellow
Write-Host ""
continueOnError: false
- interactive: false
+ interactive: true
posix:
shell: sh
run: |
@@ -37,7 +37,7 @@ hooks:
printf '\033[33m ---------------------------------------------------------------------------------------------------\033[0m\n'
printf '\n'
continueOnError: false
- interactive: false
+ interactive: true
postdeploy:
windows:
diff --git a/chat-app/backend/app/utils/voice_utils.py b/chat-app/backend/app/utils/voice_utils.py
index 196d68db..3ad20c9d 100644
--- a/chat-app/backend/app/utils/voice_utils.py
+++ b/chat-app/backend/app/utils/voice_utils.py
@@ -3,6 +3,7 @@
"""
import re
from typing import Any
+from urllib.parse import urlparse
from azure.ai.voicelive.models import AzureStandardVoice
from azure.core.credentials import AzureKeyCredential
@@ -32,21 +33,39 @@ async def resolve_credential(api_key: str | None, client_id: str | None = None)
return await get_azure_credential_async(client_id=client_id)
+def _hostname_matches(endpoint: str, suffix: str) -> bool:
+ """Return True only if the URL's hostname equals or is a subdomain of `suffix`.
+
+ Guards against incomplete URL substring sanitization (CodeQL
+ py/incomplete-url-substring-sanitization): a plain `in` check would match
+ attacker-controlled hosts like `openai.azure.com.evil.com` or paths that
+ embed the expected domain.
+ """
+ try:
+ hostname = urlparse(endpoint).hostname
+ except ValueError:
+ return False
+ if not hostname:
+ return False
+ hostname = hostname.lower()
+ suffix = suffix.lower()
+ return hostname == suffix or hostname.endswith("." + suffix)
+
+
def resolve_endpoint(voicelive_endpoint: str | None, openai_endpoint: str | None) -> str | None:
"""Pick the correct Azure OpenAI endpoint for realtime connections."""
endpoint = voicelive_endpoint or openai_endpoint
if not endpoint:
return None
- host = endpoint.lower()
# Prefer openai.azure.com host over services.ai.azure.com
- if "services.ai.azure.com" in host and openai_endpoint:
+ if _hostname_matches(endpoint, "services.ai.azure.com") and openai_endpoint:
endpoint = openai_endpoint
return endpoint
def is_valid_realtime_endpoint(endpoint: str) -> bool:
"""Check if endpoint is a valid Azure OpenAI host for realtime."""
- return "openai.azure.com" in endpoint.lower()
+ return _hostname_matches(endpoint, "openai.azure.com")
# Markdown/URL patterns for TTS text cleaning
diff --git a/documents/EmbeddableWidgetGuide.md b/documents/EmbeddableWidgetGuide.md
new file mode 100644
index 00000000..9a0eb0f3
--- /dev/null
+++ b/documents/EmbeddableWidgetGuide.md
@@ -0,0 +1,286 @@
+---
+title: Embeddable Chat Widget Guide
+description: How the chat widget is built, packaged, and embedded into a scenario host page in the Customer Chatbot Solution Accelerator
+author: Microsoft
+ms.topic: how-to
+keywords:
+ - chat widget
+ - embed
+ - shadow dom
+ - vite library mode
+---
+
+## Overview
+
+The Customer Chatbot Solution Accelerator ships a floating chat widget that any host page can render with a single script include and one initialization call. The widget is produced from the chat frontend as a standalone JavaScript bundle (`widget.js`), packaged into the scenario frontend image, and served from the scenario site's own origin. The widget mounts inside a Shadow DOM so host-page CSS does not leak into the chat UI, and it calls the chat backend directly using CORS.
+
+This guide describes how the widget is built, where it is served from, how the scenario host page loads it, and what to check when validating an embed in local development or on Azure.
+
+## Widget architecture
+
+```mermaid
+flowchart LR
+ Host[Scenario host page
scenario-frontend origin] -->|GET /widget.js| Widget[widget.js served by
scenario-frontend nginx]
+ Widget -->|window.ChatWidget.init| Shadow[Shadow DOM widget UI]
+ Shadow -->|POST /api/chat/message
CORS| ChatAPI[chat-backend App Service]
+```
+
+Three components are involved:
+
+* Chat frontend at [chat-app/frontend](../chat-app/frontend), which builds two outputs from the same source: the full chat SPA and the standalone widget bundle.
+* Scenario frontend at [scenario-app/frontend](../scenario-app/frontend), which acts as the host page that embeds the widget. Its Docker build copies `widget.js` from the chat frontend build so both are served from the same nginx.
+* Chat backend at [chat-app/backend](../chat-app/backend), which serves `POST /api/chat/*` endpoints with an explicit CORS allowlist that includes the scenario frontend origin.
+
+## How `widget.js` is built
+
+The chat frontend has two Vite configurations. The default configuration builds the full SPA, and [vite.widget.config.ts](../chat-app/frontend/vite.widget.config.ts) builds the widget bundle in library mode:
+
+```ts
+build: {
+ emptyOutDir: false,
+ outDir: 'dist',
+ sourcemap: true,
+ lib: {
+ entry: path.resolve(__dirname, 'src/widget-bootstrap.ts'),
+ name: 'ChatWidget',
+ formats: ['iife'],
+ fileName: () => 'widget.js',
+ },
+ rollupOptions: {
+ output: { inlineDynamicImports: true, banner: 'var process={env:{NODE_ENV:"production"}};' },
+ },
+}
+```
+
+Key properties of the bundle:
+
+* IIFE format so the file works with a plain `
+
+```
+
+Then add the host origin to the chat backend's `ALLOWED_ORIGINS_STR` so CORS lets the widget call the API.
+
+### Option 2: Cross-origin script load
+
+If you cannot bundle the widget, load it from the chat frontend origin:
+
+```html
+
+```
+
+Requirements:
+
+* Configure the chat frontend to serve `widget.js` from a stable path. Today the chat frontend Dockerfile builds it but no scenario currently loads it cross-origin, so treat this path as a customization rather than a supported default.
+* Add the host origin to `ALLOWED_ORIGINS_STR` on the chat backend.
+* Confirm the host page's Content Security Policy allows scripts from the chat frontend origin.
+
+> [!NOTE]
+> The accelerator's default deployment topology only wires the same-origin variant. Choose the cross-origin variant only if you have a specific reason not to bundle the widget.
+
+## Validation checklist
+
+Use this list to confirm an embed works end to end.
+
+1. `/widget.js` returns HTTP 200 from the host page origin and is not blocked by extensions or CSP.
+2. Browser console shows no CORS errors on `POST /api/chat/message`.
+3. `window.ChatWidget` is defined after the script loads.
+4. The chat launcher renders in the bottom corner of the page, and opening the panel shows a shadow root under `
`.
+5. The chat backend `ALLOWED_ORIGINS_STR` includes the exact host page origin, including scheme and port when relevant.
+6. `window.__RUNTIME_CONFIG__.VITE_CHAT_API_BASE_URL` in devtools resolves to the deployed chat backend URL, not `http://localhost:8000`.
+
+## Related documents
+
+| Document | Use |
+|----------------------------------------------------------------------------------------------|------------------------------------------------------------------|
+| [scenario-deployment-guide.md](scenario-deployment-guide.md) | End-to-end deployment steps for a scenario and its chat backend |
+| [TechnicalArchitecture.md](TechnicalArchitecture.md) | Broader architecture of the accelerator |
+| [LocalDevelopmentSetup.md](LocalDevelopmentSetup.md) | Full local dev environment setup for both apps |
diff --git a/documents/chatplan.md b/documents/chatplan.md
deleted file mode 100644
index ffa2b6ea..00000000
--- a/documents/chatplan.md
+++ /dev/null
@@ -1,457 +0,0 @@
-Best Overall Approach for Your Scenario
-Use:
-
-
-React + Fluent UI for the widget UI
-
-
-Vite library build
-
-
-Simple script embed
-
-
-FastAPI backend
-
-
-Azure App Service for everything
-
-
-Optional iframe only if styling issues appear
-
-
-Do not over-engineer:
-
-
-multi-tenant systems
-
-
-Web PubSub
-
-
-microservices
-
-
-CDN complexity
-
-
-complex auth
-
-
-distributed session architectures
-
-
-For a demo accelerator, simplicity wins.
-
-Recommended Final Architecture
-Resource Group|+-- ecommerce-frontend-appservice|+-- ecommerce-backend-fastapi|+-- ai-widget-frontend-appservice|+-- ai-widget-backend-fastapi
-Then the ecommerce site embeds:
-
-That’s it.
-
-Frontend Recommendation (Revised)
-Keep Fluent UI
-Since:
-
-
-you already use it
-
-
-it matches your ecosystem
-
-
-internal consistency matters
-
-
-There’s no reason to switch.
-Fluent UI works perfectly fine for an embedded widget.
-
-Recommended Widget Frontend Architecture
-Use Vite Library Mode
-This is the key simplification.
-Instead of deploying a full SPA, build:
-
-
-one embeddable JS bundle
-
-
-mounted via script tag
-
-
-Example:
-class ChatWidget { init() { const root = document.createElement("div") document.body.appendChild(root) createRoot(root).render(
) }}window.ChatWidget = new ChatWidget()
-
-Best Isolation Strategy for Demo
-Start WITHOUT iframe
-Use:
-
-
-React
-
-
-Fluent UI
-
-
-Shadow DOM
-
-
-This gives:
-
-
-enough CSS isolation
-
-
-easier implementation
-
-
-simpler messaging
-
-
-simpler auth
-
-
-easier debugging
-
-
-
-Revised Recommendation
-Preferred Architecture
-Script Loader + Shadow DOM
-ecommerce site | +-- loads widget.js | +-- creates shadow root | +-- mounts React app
-This is the sweet spot for your use case.
-
-Why NOT iframe Initially?
-iframe adds:
-
-
-resizing complexity
-
-
-cross-window messaging
-
-
-auth passing
-
-
-mobile quirks
-
-
-focus issues
-
-
-For a demo/accelerator:
-
-
-probably unnecessary
-
-
-You can always migrate later.
-
-Backend Recommendation (Simplified)
-FastAPI Still Ideal
-Keep:
-
-
-one FastAPI service for AI widget backend
-
-
-Use it for:
-
-
-chat API
-
-
-Azure OpenAI calls
-
-
-streaming responses
-
-
-session state
-
-
-
-Simplified Backend Architecture
-widget frontend | +-- FastAPI backend | +-- Azure OpenAI
-That’s enough for a very solid accelerator.
-
-Best Streaming Approach
-Use Server-Sent Events (SSE)
-Do not start with WebSockets.
-SSE is:
-
-
-dramatically simpler
-
-
-works great for AI streaming
-
-
-perfect with FastAPI
-
-
-FastAPI SSE example:
-from sse_starlette.sse import EventSourceResponse
-This is the current practical sweet spot.
-
-Azure Infra Recommendation (Simplified)
-Keep Everything on App Service
-This is important.
-Do NOT prematurely introduce:
-
-
-AKS
-
-
-Container Apps
-
-
-Functions
-
-
-Front Door
-
-
-Static Web Apps
-
-
-Unless you specifically need them.
-
-Ideal Azure Setup for Demo
-1. Widget Frontend
-Azure App Service
-
-
-static Vite build served via nginx/node
-
-
-OR
-Even simpler:
-
-
-serve built assets directly from FastAPI
-
-
-
-2. Widget Backend
-Azure App Service (Linux)
-
-
-FastAPI
-
-
-Uvicorn/Gunicorn
-
-
-
-My Actual Recommendation
-Simplest + Best Demo Setup
-ai-widget-fastapi-appservice | +-- serves API +-- serves widget.js +-- serves static assets
-Single deploy.
-This is probably the highest ROI setup for you.
-
-Why This Is Better for a Demo
-Because deployment becomes:
-az webapp up
-Instead of:
-
-
-coordinating frontend/backend deploys
-
-
-handling CORS headaches
-
-
-multiple origins
-
-
-Front Door config
-
-
-
-Recommended File Structure
-widget-project/|+-- frontend/| +-- React widget|+-- backend/| +-- FastAPI|+-- backend/static/widget/ +-- built frontend assets
-FastAPI serves:
-
-
-/widget.js
-
-
-/assets/*
-
-
-/api/chat
-
-
-
-Embedding API Recommendation
-Dead Simple Embed
-
-
-Recommended Widget UX
-For demo quality:
-
-
-floating bottom-right launcher
-
-
-expandable panel
-
-
-streaming text
-
-
-markdown support
-
-
-citations/products optional
-
-
-That’s enough to feel production-grade.
-
-Suggested Frontend Libraries
-Keep It Lean
-Use
-
-
-React
-
-
-Fluent UI
-
-
-Zustand
-
-
-react-markdown
-
-
-Avoid
-
-
-Redux
-
-
-Tailwind + Fluent together
-
-
-giant animation libs
-
-
-complex routing
-
-
-Widgets should stay tiny.
-
-Important Implementation Detail
-Use Shadow DOM
-Even for demos.
-Example:
-const host = document.createElement("div")document.body.appendChild(host)const shadowRoot = host.attachShadow({ mode: "open" })
-Mount React inside shadow root.
-This avoids:
-
-
-ecommerce CSS collisions
-
-
-z-index nightmares
-
-
-button resets
-
-
-font inheritance chaos
-
-
-
-My Final Recommendation
-If I were building your exact accelerator:
-I would do:
-Frontend
-
-
-React
-
-
-Fluent UI
-
-
-Vite library build
-
-
-Shadow DOM
-
-
-Single floating widget
-
-
-Backend
-
-
-FastAPI
-
-
-SSE streaming
-
-
-Azure OpenAI integration
-
-
-Infra
-
-
-ONE Azure App Service
-
-
-Frontend assets served by FastAPI
-
-
-Embed
-
-
-simple
-```
-
-Optional single-app variant for fastest demo iteration:
-
-```text
-ai-widget-fastapi-appservice
- -> serves /widget.js and /assets/*
- -> serves /api/chat (SSE)
-```
-
----
-
-## Related documents
-
-| Document | Use |
-|----------|-----|
-| [`src/separationPlan.md`](../src/separationPlan.md) | Infrastructure, **`postprovision`**, §11 validation. |
-| [`embeddable-chat-widget-technical-plan.md`](embeddable-chat-widget-technical-plan.md) | Shadow DOM embed, FastAPI/SSE, CORS, packaging. |
-
----
-
-## Review cadence
-
-| Frequency | Audience | Focus |
-|-----------|----------|--------|
-| Monthly | Engineering, platform | Releases, **`azd`**, hooks, regressions |
-| Six weeks | Product, UX | Widget UX, accessibility, performance budgets |
-| Quarterly | Partnerships | Scenario packs, onboarding, non-retail rollout |
diff --git a/documents/embeddable-chat-widget-technical-plan.md b/documents/embeddable-chat-widget-technical-plan.md
deleted file mode 100644
index 99de1958..00000000
--- a/documents/embeddable-chat-widget-technical-plan.md
+++ /dev/null
@@ -1,232 +0,0 @@
-# Technical plan: embeddable Chat widget (chat-app origins, any-host embed)
-
-This document specifies how to deliver a **small, embeddable client** sourced from **chat-app** capabilities, **without merging** chat and ecommerce repos. Host sites (starting with ecommerce-app, later arbitrary domains) load the widget the same way a third-party would.
-
-Product milestones and roadmap context live in **[customer-chatbot-product-roadmap.md](customer-chatbot-product-roadmap.md)**.
-
-**Progress snapshot (13 May 2026):** Local end-to-end works for the full **chat-app** SPA and for the widget embedded in **ecommerce-app** (script load, Shadow DOM, chat API). Hosted **Azure** path is wired in **`infra_basic`** (widget ships with the chat frontend image; ecommerce gets runtime URLs for widget script + chat API). Remaining gaps vs this plan: **SSE streaming**, **`rid` / OTel embed correlation**, **M4 polish (SRI, iframe fallback)**, and validation that **Foundry agent names** and **Easy Auth cross-origin** behave as expected in production. Details: [§11](#11-implementation-progress-and-deploy-readiness).
-
-## 1. Objectives
-
-- One **distribution artifact** developers can paste into arbitrary HTML (script embed).
-- **Runtime configuration** only (API base URL, optional tenant/widget id, theme tokens). No compile-time coupling to ecommerce-app.
-- **Security-first**: predictable CORS/auth story, CSP-friendly embedding options, minimized XSS surface.
-- **Parity**: reuse chat backend routes already used by full chat SPA (`/api/chat/*`, auth, Voice Live where applicable).
-- **Simplicity first**: architecture optimized for demo velocity on Azure App Service.
-
-## 2. Non-goals
-
-- Folding chat UI into ecommerce-app bundles or importing ecommerce source into chat.
-- Replacing the full-screen chat SPA except as a **standalone** fallback for unsupported browsers or iframe-blocked contexts.
-- Designing full multi-tenant control planes in initial release.
-- Introducing Web PubSub, microservices, AKS, Front Door, or distributed session architecture for MVP.
-
-## 3. Embedding model (recommended stack)
-
-Default to one pattern and keep one fallback.
-
-### 3.1 Primary: script loader + Shadow DOM (recommended)
-
-| Concern | How it is addressed |
-|--------|----------------------|
-| **CSS isolation** | Widget mounts inside a Shadow Root to isolate host CSS and reduce style collisions. |
-| **Embed simplicity** | Host adds one script include and calls `ChatWidget.init(...)`. |
-| **Debugging** | Single-window runtime avoids cross-window message inspection for initial MVP. |
-| **Versioning** | `widget.js` path/version controls rollout; host integration remains stable. |
-
-**Mechanics**
-
-1. Host page loads `https://
/widget.js`.
-2. `widget.js` creates a host node, attaches Shadow Root, and mounts React app.
-3. Widget calls FastAPI APIs on same origin or configured `apiBaseUrl`.
-
-Reference shape:
-
-```text
-class ChatWidget {
- init() {
- const host = document.createElement("div")
- document.body.appendChild(host)
- const shadowRoot = host.attachShadow({ mode: "open" })
- createRoot(shadowRoot).render()
- }
-}
-window.ChatWidget = new ChatWidget()
-```
-
-### 3.2 Fallback: iframe loader (only when required)
-
-Use iframe only if a target host has blocking CSS/policy behavior that Shadow DOM cannot address safely.
-
-## 4. Frontend architecture
-
-```mermaid
-flowchart LR
- HostPage[Host ecommerce or external site]
- Loader[widget.js loader]
- Shadow[ShadowRoot React widget]
- Api[chat FastAPI backend]
- HostPage --> Loader
- Loader --> Shadow
- Shadow --> Api
-```
-
-**Frontend build targets**
-
-- **Vite library mode** for `widget.js` bundle.
-- React + Fluent UI component tree focused on floating launcher + panel.
-- No full SPA routing for widget package.
-
-Example embed API:
-
-```html
-
-
-```
-
-## 5. Backend and CORS contract
-
-Widget backend remains FastAPI.
-
-Primary API scope:
-
-- `POST /api/chat` or streaming equivalent.
-- Azure OpenAI orchestration.
-- Session state for active widget conversation.
-
-Streaming recommendation:
-
-- Use SSE first (`EventSourceResponse`) instead of WebSockets.
-- Keep protocol simple for incremental token streaming.
-
-CORS:
-
-- Explicit host allowlist only.
-- No wildcard `*` for credentialed flows.
-
-## 6. Observability and operations
-
-- Widget requests carry **`rid=`** correlation id propagated to OTel **`embed.request_id`**.
-- Feature flags (`widget.voice_live.enabled`) backend-driven to avoid mismatched UX.
-
-## 7. Packaging and delivery
-
-| Artifact | Host |
-|----------|------|
-| `widget.js` | App Service static path (preferred for MVP) |
-| static assets (`/assets/*`) | Same App Service as widget backend or widget frontend |
-| Integrity | **`SRI hash`** published in README + changelog |
-
-Semantic versioning **`MAJOR`** for breaking **`postMessage`** or config schema.
-
-### Deployment options
-
-Preferred for fastest demo loop:
-
-```text
-ai-widget-fastapi-appservice
- -> /widget.js
- -> /assets/*
- -> /api/chat (SSE)
-```
-
-Alternative (keep current four-app split):
-
-```text
-ResourceGroup
- -> ecommerce-frontend-appservice
- -> ecommerce-backend-fastapi
- -> ai-widget-frontend-appservice
- -> ai-widget-backend-fastapi
-```
-
-## 8. Milestones (technical)
-
-Sequencing aligns with later roadmap milestones (**Embed widget MVP** onward) in [customer-chatbot-product-roadmap.md](customer-chatbot-product-roadmap.md).
-
-### M1 Widget shell
-
-- Vite library build for `widget.js`.
-- Shadow DOM mount + floating launcher/panel UX.
-- Script embed + `ChatWidget.init({ apiBaseUrl, theme })`.
-
-### M2 Backend hardening for third-party origins
-
-- SSE endpoint for streamed responses.
-- Explicit origin allowlist and simplified token/session flow.
-- Keep auth straightforward for accelerator demo scope.
-
-### M3 ecommerce-app integration PoC
-
-- Add script include to **`ecommerce-app/frontend`** and initialize widget.
-- Validate CSS isolation, mobile behavior, and focus/keyboard interactions.
-
-### M4 Polish and CDN
-
-- SRI, minified bundle, Lighthouse budget, error boundary UX.
-- Introduce iframe variant only if required by host constraints.
-
-### M5 Extensions
-
-- Optional tenant mapping, partner snippets, and advanced entitlements.
-- Defer distributed auth/session patterns until scale requires them.
-
-## 9. Risks and mitigations
-
-| Risk | Mitigation |
-|------|------------|
-| Host CSS affects widget | Shadow DOM by default. |
-| Cookie SameSite failures | Prefer simple token/session flow; avoid cross-site cookie dependence for MVP. |
-| Style drift | Single Fluent theme manifest shared between SPA and widget build (shared token JSON if needed). |
-
-## 10. References in repo
-
-- Chat UI entry: [`chat-app/frontend`](../chat-app/frontend)
-- Widget bundle: [`chat-app/frontend/vite.widget.config.ts`](../chat-app/frontend/vite.widget.config.ts), [`chat-app/frontend/src/widget-bootstrap.ts`](../chat-app/frontend/src/widget-bootstrap.ts), [`chat-app/frontend/src/widget.tsx`](../chat-app/frontend/src/widget.tsx), [`chat-app/frontend/src/WidgetApp.tsx`](../chat-app/frontend/src/WidgetApp.tsx)
-- Ecommerce embed: [`ecommerce-app/frontend/src/embedChatWidget.ts`](../ecommerce-app/frontend/src/embedChatWidget.ts)
-- Separation context: [`src/separationPlan.md`](../src/separationPlan.md)
-- Cloud deploy / CORS: [`infra_basic/main.bicep`](../infra_basic/main.bicep) app settings
-
-## 11. Implementation progress and deploy readiness
-
-### 11.1 Where things stand vs milestones
-
-| Milestone | Plan intent | Current state |
-|-----------|-------------|---------------|
-| **M1 Widget shell** | Vite library `widget.js`, Shadow DOM, `ChatWidget.init`, launcher/panel | **Done.** Second Vite build ([`chat-app/frontend/vite.widget.config.ts`](../chat-app/frontend/vite.widget.config.ts)) outputs IIFE `widget.js` from [`widget-bootstrap.ts`](../chat-app/frontend/src/widget-bootstrap.ts). [`widget.tsx`](../chat-app/frontend/src/widget.tsx) mounts Shadow DOM, inlines CSS, sets API base + embed auth base. [`WidgetApp.tsx`](../chat-app/frontend/src/WidgetApp.tsx) provides floating control + panel and reuses **`ChatSidebar`** (text + voice hooks same as SPA stack). |
-| **M2 Backend / third-party** | SSE, explicit CORS, straightforward auth | **Partial.** CORS is explicit (`allow_credentials=True`, no `*`); unified deploy sets chat API **`ALLOWED_ORIGINS_STR`** to **both** chat and ecommerce frontend origins ([`infra_basic/main.bicep`](../infra_basic/main.bicep) chat backend block). Chat still uses **`POST /api/chat/message`** with a **single JSON** assistant reply ([`chat-app/backend/app/routers/chat.py`](../chat-app/backend/app/routers/chat.py)), not **SSE** as recommended in §5. Optional auth path exists; embed uses **`/.auth/me`** on the **widget script origin** ([`AuthContext.tsx`](../chat-app/frontend/src/contexts/AuthContext.tsx) + [`embedContext.ts`](../chat-app/frontend/src/lib/embedContext.ts)). **§6** correlation (**`rid` → `embed.request_id`**) is **not** implemented. |
-| **M3 Ecommerce PoC** | Script on ecommerce, validate isolation / mobile / a11y | **Done for integration plumbing.** [`ecommerce-app/frontend/src/embedChatWidget.ts`](../ecommerce-app/frontend/src/embedChatWidget.ts) injects `widget.js`, then calls `ChatWidget.init` with **`VITE_CHAT_API_BASE_URL`** / theme from env or **`window.__RUNTIME_CONFIG__`**. [`main.tsx`](../ecommerce-app/frontend/src/main.tsx) invokes **`embedChatWidget()`**. Dev server serves built `widget.js` from chat `dist` via [`ecommerce-app/frontend/vite.config.ts`](../ecommerce-app/frontend/vite.config.ts). Formal **mobile / keyboard / a11y** sign-off not recorded here. |
-| **M4 Polish** | SRI, minify/Lighthouse, iframe fallback | **Not done** (widget build still emits **sourcemaps** in [`vite.widget.config.ts`](../chat-app/frontend/vite.widget.config.ts); no published **SRI** hash; **iframe** loader not built). |
-| **M5 Extensions** | Tenants, partner snippets | **Not started.** |
-
-### 11.2 Repo map (implemented pieces)
-
-| Area | Location |
-|------|----------|
-| Widget library entry + `init` | [`chat-app/frontend/src/widget-bootstrap.ts`](../chat-app/frontend/src/widget-bootstrap.ts) |
-| Shadow mount + config | [`chat-app/frontend/src/widget.tsx`](../chat-app/frontend/src/widget.tsx) |
-| Widget UI | [`chat-app/frontend/src/WidgetApp.tsx`](../chat-app/frontend/src/WidgetApp.tsx) |
-| SPA + widget API base override | [`chat-app/frontend/src/lib/api.ts`](../chat-app/frontend/src/lib/api.ts) |
-| Host embed loader | [`ecommerce-app/frontend/src/embedChatWidget.ts`](../ecommerce-app/frontend/src/embedChatWidget.ts) |
-| Ecommerce runtime injection (Azure hostnames) | [`ecommerce-app/frontend/startup.sh`](../ecommerce-app/frontend/startup.sh) |
-| Chat image includes SPA + `widget.js` | [`chat-app/frontend/Dockerfile`](../chat-app/frontend/Dockerfile) (`npm run build` → `dist/` copied to nginx) |
-| Infra: ecommerce → chat widget + API URLs | [`infra_basic/main.bicep`](../infra_basic/main.bicep) (`VITE_CHAT_WIDGET_ORIGIN`, `VITE_CHAT_API_BASE_URL` on ecommerce frontend module) |
-
-### 11.3 Ready to deploy and see it end-to-end?
-
-**You are in good shape to try a hosted run** if images are current and post-provision agent settings are populated: the **same** `npm run build` that the chat frontend Dockerfile already runs produces **`widget.js`** alongside the SPA, and ecommerce startup / Bicep supply the **chat frontend origin** (script) and **chat API base** (XHR).
-
-**Checklist before calling hosted embed “done”:**
-
-1. **Images** — Redeploy **chat-frontend** after any widget change so **`/widget.js`** on the chat site matches the bundle you tested.
-2. **Browser network** — From the **ecommerce** origin, confirm **`GET https:///widget.js`**, then **`POST https:///api/chat/...`** without CORS errors (chat API allowlist already includes **both** frontends in **`infra_basic`**).
-3. **AI path** — Chat backend needs working **Foundry** (or equivalent) config for real replies; template leaves agent name env placeholders in some paths—confirm **`FOUNDRY_*_AGENT`** (or your post-provision automation) matches deployed agents.
-4. **Auth** — Widget may run as **guest** unless **`fetch` to `https:///.auth/me`** from the ecommerce page succeeds (cross-origin **cookies + CORS** on the chat App Service). Treat signed-in parity as **environment-dependent** until verified.
-5. **Custom domains** — If you move off `*.azurewebsites.net`, update **`ALLOWED_ORIGINS_STR`** and the **`VITE_*`** / runtime URLs accordingly.
-
-**Summary:** Local **M1 + M3** goals are met; **M2** is partially met (CORS yes, SSE and embed **`rid`** no); **M4–M5** are open. You can **deploy and smoke-test** the embed on Azure; treat **streaming**, **observability correlation**, and **M4** items as follow-up work, not blockers for a first **“see it live”** pass.
diff --git a/infra/scripts/pre-provision/preflight_scenario.ps1 b/infra/scripts/pre-provision/preflight_scenario.ps1
index e031d7c1..6f14d721 100644
--- a/infra/scripts/pre-provision/preflight_scenario.ps1
+++ b/infra/scripts/pre-provision/preflight_scenario.ps1
@@ -18,4 +18,4 @@ if (-not (Test-Path -LiteralPath $manifestPath)) {
}
Write-Host "Deployment scenario: $scenario"
-Write-Host "Set AZURE_ENV_SCENARIO before the first azd up on a new environment (default is ecommerce)."
+Write-Host "To deploy a different scenario, set AZURE_ENV_SCENARIO to one of: ecommerce, healthcare, banking before your first 'azd up' on this environment (default is ecommerce)."
diff --git a/infra/scripts/pre-provision/preflight_scenario.sh b/infra/scripts/pre-provision/preflight_scenario.sh
index 3aa8fa69..f872b334 100644
--- a/infra/scripts/pre-provision/preflight_scenario.sh
+++ b/infra/scripts/pre-provision/preflight_scenario.sh
@@ -22,4 +22,4 @@ if [[ ! -f "$MANIFEST_PATH" ]]; then
fi
echo "Deployment scenario: $scenario"
-echo "Set AZURE_ENV_SCENARIO before the first azd up on a new environment (default is ecommerce)."
+echo "To deploy a different scenario, set AZURE_ENV_SCENARIO to one of: ecommerce, healthcare, banking before your first 'azd up' on this environment (default is ecommerce)."
diff --git a/scenario-app/backend/app/utils/voice_utils.py b/scenario-app/backend/app/utils/voice_utils.py
index 196d68db..3ad20c9d 100644
--- a/scenario-app/backend/app/utils/voice_utils.py
+++ b/scenario-app/backend/app/utils/voice_utils.py
@@ -3,6 +3,7 @@
"""
import re
from typing import Any
+from urllib.parse import urlparse
from azure.ai.voicelive.models import AzureStandardVoice
from azure.core.credentials import AzureKeyCredential
@@ -32,21 +33,39 @@ async def resolve_credential(api_key: str | None, client_id: str | None = None)
return await get_azure_credential_async(client_id=client_id)
+def _hostname_matches(endpoint: str, suffix: str) -> bool:
+ """Return True only if the URL's hostname equals or is a subdomain of `suffix`.
+
+ Guards against incomplete URL substring sanitization (CodeQL
+ py/incomplete-url-substring-sanitization): a plain `in` check would match
+ attacker-controlled hosts like `openai.azure.com.evil.com` or paths that
+ embed the expected domain.
+ """
+ try:
+ hostname = urlparse(endpoint).hostname
+ except ValueError:
+ return False
+ if not hostname:
+ return False
+ hostname = hostname.lower()
+ suffix = suffix.lower()
+ return hostname == suffix or hostname.endswith("." + suffix)
+
+
def resolve_endpoint(voicelive_endpoint: str | None, openai_endpoint: str | None) -> str | None:
"""Pick the correct Azure OpenAI endpoint for realtime connections."""
endpoint = voicelive_endpoint or openai_endpoint
if not endpoint:
return None
- host = endpoint.lower()
# Prefer openai.azure.com host over services.ai.azure.com
- if "services.ai.azure.com" in host and openai_endpoint:
+ if _hostname_matches(endpoint, "services.ai.azure.com") and openai_endpoint:
endpoint = openai_endpoint
return endpoint
def is_valid_realtime_endpoint(endpoint: str) -> bool:
"""Check if endpoint is a valid Azure OpenAI host for realtime."""
- return "openai.azure.com" in endpoint.lower()
+ return _hostname_matches(endpoint, "openai.azure.com")
# Markdown/URL patterns for TTS text cleaning