From 60123024ba9aae96737104cb0a1982d3a62a56fb Mon Sep 17 00:00:00 2001 From: LauraGPT Date: Sun, 26 Jul 2026 20:54:36 +0000 Subject: [PATCH 1/6] docs: design trusted browser CORS --- .../2026-07-27-funasr-server-cors-design.md | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-27-funasr-server-cors-design.md diff --git a/docs/superpowers/specs/2026-07-27-funasr-server-cors-design.md b/docs/superpowers/specs/2026-07-27-funasr-server-cors-design.md new file mode 100644 index 000000000..9bbd51c5d --- /dev/null +++ b/docs/superpowers/specs/2026-07-27-funasr-server-cors-design.md @@ -0,0 +1,84 @@ +# FunASR Server Trusted Browser CORS Design + +Date: 2026-07-27 + +## Context + +Browser clients such as NextChat send OpenAI-compatible multipart transcription requests directly to a local `funasr-server`. The current server returns a valid transcription to command-line clients, but it does not return CORS headers. A browser therefore hides the successful response, and requests with an `Authorization` header fail their preflight with HTTP 405. + +## Goals + +- Let operators explicitly authorize one or more browser origins. +- Keep the current no-CORS behavior when no option is supplied. +- Support both simple multipart requests and preflighted requests with bearer tokens. +- Keep the server usable through the Python `create_app` API and the `funasr-server` CLI. +- Document a reproducible local-browser configuration in English and Chinese. + +## Non-Goals + +- Do not enable permissive CORS by default. +- Do not add authentication, cookies, an HTTP proxy, or an origin regular expression. +- Do not change transcription behavior or model loading. +- Do not make the NextChat server proxy requests to a user's local machine. + +## Interface + +The CLI gains a repeatable option: + +```bash +funasr-server \ + --device cpu \ + --model sensevoice \ + --cors-origin http://localhost:3000 \ + --cors-origin http://127.0.0.1:3000 +``` + +`create_app` gains a backward-compatible optional parameter: + +```python +def create_app( + device: str = "cuda", + preload_model: str = "auto", + model_path: str | None = None, + hub: str = "ms", + cors_origins: list[str] | None = None, +) -> FastAPI: +``` + +Empty values are ignored, surrounding whitespace is removed, and duplicate origins retain first-seen order. Passing `*` is allowed only as an explicit operator choice. + +## Middleware Policy + +When the normalized origin list is non-empty, the app adds Starlette's `CORSMiddleware` with: + +- `allow_origins`: the normalized exact origins +- `allow_methods`: `GET`, `POST`, and `OPTIONS` +- `allow_headers`: `Authorization` and `Content-Type` +- `allow_credentials`: `False` + +When the list is empty or omitted, no middleware is installed. This preserves the existing security boundary and response behavior. + +## Data Flow + +1. The operator supplies trusted browser origins on the CLI or to `create_app`. +2. The server normalizes and de-duplicates the values. +3. CORS middleware answers matching preflight requests before route dispatch. +4. The existing transcription route processes the multipart audio unchanged. +5. Middleware adds the matching `Access-Control-Allow-Origin` response header. + +## Error Handling + +An unlisted origin receives no CORS authorization header. The server still behaves normally for non-browser clients. CLI parsing remains responsible for option shape; origin reachability is not checked at startup because a valid browser origin may be offline when the service starts. + +## Verification + +- Unit tests prove default-disabled behavior and exact middleware configuration. +- CLI tests prove repeated `--cors-origin` values reach `create_app` unchanged. +- Existing server tests prove model and transcription behavior is unchanged. +- A real CPU server must return a successful matching-origin preflight and a real SenseVoice transcription with CORS headers. +- A request from an unlisted origin must not receive `Access-Control-Allow-Origin`. +- English and Chinese docs must include the explicit trusted-origin command and avoid recommending wildcard access. + +## Rollback + +The design, implementation, and documentation are separate signed commits. The feature branch is preserved remotely before merge, and the default-disabled behavior allows operators to remove the option without changing any other server configuration. From a0e90e4ff03f5107740e4228564477bec07b6ae6 Mon Sep 17 00:00:00 2001 From: LauraGPT Date: Sun, 26 Jul 2026 20:56:26 +0000 Subject: [PATCH 2/6] docs: plan trusted browser CORS --- .../plans/2026-07-27-funasr-server-cors.md | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-27-funasr-server-cors.md diff --git a/docs/superpowers/plans/2026-07-27-funasr-server-cors.md b/docs/superpowers/plans/2026-07-27-funasr-server-cors.md new file mode 100644 index 000000000..6ab31a3b6 --- /dev/null +++ b/docs/superpowers/plans/2026-07-27-funasr-server-cors.md @@ -0,0 +1,250 @@ +# Trusted Browser CORS Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add explicit, default-disabled trusted browser origins to `funasr-server` so local web applications can consume successful OpenAI-compatible transcription responses. + +**Architecture:** The CLI collects repeated exact origins and passes them into `create_app`. The app normalizes the list and conditionally installs FastAPI/Starlette `CORSMiddleware`; transcription routes and model loading remain unchanged. + +**Tech Stack:** Python, argparse, FastAPI, Starlette CORS middleware, pytest, curl. + +## Global Constraints + +- No CORS middleware is installed unless at least one non-empty origin is supplied. +- Allowed methods are exactly `GET`, `POST`, and `OPTIONS`. +- Allowed headers are exactly `Authorization` and `Content-Type`. +- `allow_credentials` remains `False`. +- Existing model loading and transcription response behavior must not change. +- Every behavior change follows red-green TDD. + +--- + +### Task 1: Conditional CORS Middleware + +**Files:** +- Modify: `tests/test_server_app_openai_segments.py` +- Modify: `funasr/bin/_server_app.py` + +**Interfaces:** +- Consumes: optional `cors_origins` iterable supplied by callers +- Produces: `create_app(..., cors_origins=None) -> FastAPI` with conditional middleware + +- [ ] **Step 1: Extend the FastAPI test stub and write failing tests** + +Add middleware capture to `DummyFastAPI` and stub `fastapi.middleware.cors.CORSMiddleware`: + +```python +class DummyFastAPI: + def __init__(self, *args, **kwargs): + self.state = types.SimpleNamespace() + self.routes = {} + self.metadata = kwargs + self.middleware = [] + + def add_middleware(self, middleware_class, **kwargs): + self.middleware.append((middleware_class, kwargs)) + +class DummyCORSMiddleware: + pass +``` + +Register the middleware stubs in `sys.modules`, then add behavior tests with hand-derived literal expectations: + +```python +def test_server_cors_is_disabled_by_default(monkeypatch): + module = load_server_app(monkeypatch) + install_dummy_funasr(monkeypatch) + + app = module.create_app(device="cpu", preload_model="sensevoice") + + assert app.middleware == [] + + +def test_server_configures_normalized_trusted_origins(monkeypatch): + module = load_server_app(monkeypatch) + install_dummy_funasr(monkeypatch) + + app = module.create_app( + device="cpu", + preload_model="sensevoice", + cors_origins=[ + " http://localhost:3000 ", + "http://localhost:3000", + "http://127.0.0.1:3000", + " ", + ], + ) + + assert app.middleware == [ + ( + module.CORSMiddleware, + { + "allow_origins": [ + "http://localhost:3000", + "http://127.0.0.1:3000", + ], + "allow_credentials": False, + "allow_methods": ["GET", "POST", "OPTIONS"], + "allow_headers": ["Authorization", "Content-Type"], + }, + ) + ] +``` + +- [ ] **Step 2: Run tests and verify RED** + +Run: + +```bash +python -m pytest tests/test_server_app_openai_segments.py -q +``` + +Expected: the new test fails because `create_app` does not accept `cors_origins`. + +- [ ] **Step 3: Implement the minimum middleware behavior** + +Import `CORSMiddleware`, add the optional `cors_origins` parameter, normalize with first-seen de-duplication, and call: + +```python +app.add_middleware( + CORSMiddleware, + allow_origins=normalized_origins, + allow_credentials=False, + allow_methods=["GET", "POST", "OPTIONS"], + allow_headers=["Authorization", "Content-Type"], +) +``` + +- [ ] **Step 4: Run the focused tests and verify GREEN** + +Run the same pytest command. Expected: all focused tests pass. + +- [ ] **Step 5: Commit the tested middleware** + +Stage only the two task files and create a signed commit named `feat(server): allow trusted browser origins`. + +### Task 2: Repeatable CLI Option + +**Files:** +- Modify: `tests/test_server_app_openai_segments.py` +- Modify: `funasr/bin/server.py` + +**Interfaces:** +- Consumes: repeated `--cors-origin ORIGIN` arguments +- Produces: `args.cors_origin: list[str] | None`, passed as `cors_origins=args.cors_origin` + +- [ ] **Step 1: Write a failing CLI forwarding test** + +Extract the existing parser construction into `build_parser()` and add a parser behavior test: + +```python +def test_server_cli_collects_repeated_cors_origins(): + module = load_server_cli() + + args = module.build_parser().parse_args( + [ + "--cors-origin", + "http://localhost:3000", + "--cors-origin", + "http://127.0.0.1:3000", + ] + ) + + assert args.cors_origin == [ + "http://localhost:3000", + "http://127.0.0.1:3000", + ] +``` + +- [ ] **Step 2: Run the single test and verify RED** + +Expected: argparse rejects `--cors-origin`. + +- [ ] **Step 3: Add the repeatable CLI argument** + +Move the current parser setup into `build_parser()`, use the parser from `main()`, and add: + +```python +parser.add_argument( + "--cors-origin", + action="append", + default=None, + metavar="ORIGIN", + help="Trusted browser origin for CORS; repeat for multiple origins (disabled by default)", +) +``` + +Pass the parsed list into the real application boundary: + +```python +app = create_app( + device=args.device, + preload_model=args.model, + model_path=args.model_path, + hub=args.hub, + cors_origins=args.cors_origin, +) +``` + +- [ ] **Step 4: Run focused and complete server tests** + +Expected: CLI forwarding and all server tests pass. + +- [ ] **Step 5: Commit the tested CLI surface** + +Create a signed commit named `feat(server): expose trusted CORS origins`. + +### Task 3: Operator Documentation + +**Files:** +- Modify: `docs/troubleshooting.md` +- Modify: `docs/troubleshooting_zh.md` + +**Interfaces:** +- Consumes: public `--cors-origin` CLI option +- Produces: bilingual, copy-pasteable browser deployment guidance + +- [ ] **Step 1: Add concise English and Chinese guidance** + +Document the browser CORS symptom, a trusted-origin startup command, repeated origins, and the requirement to use the browser's exact scheme/host/port. + +- [ ] **Step 2: Review the rendered Markdown contract** + +Confirm both commands use the same public CLI, both explain exact origins, neither recommends wildcard access, and neither claims CORS is enabled by default. + +- [ ] **Step 3: Run docs and server verification** + +Run the focused docs tests, all server tests, Black, Ruff, compilation, and `git diff --check`. + +- [ ] **Step 4: Commit documentation** + +Create a signed commit named `docs(server): explain browser CORS setup`. + +### Task 4: Real Browser-Contract Smoke and Publication + +**Files:** +- No production file changes unless verification exposes a tested defect. + +**Interfaces:** +- Consumes: exact feature branch head +- Produces: live preflight/transcription evidence and a reviewable FunASR PR + +- [ ] **Step 1: Start the exact branch server on CPU** + +Use SenseVoice and `--cors-origin http://127.0.0.1:3000` on an unused port. + +- [ ] **Step 2: Verify matching and non-matching origins** + +Assert matching-origin `OPTIONS` returns 200 with the expected allow-origin/method/header values, matching-origin multipart POST returns 200 with transcription text and allow-origin, and an unlisted origin receives no allow-origin header. + +- [ ] **Step 3: Verify complete repository gates** + +Run relevant tests, formatting, lint, compilation, signature checks, and diff checks at the exact head. + +- [ ] **Step 4: Push with rollback protection and open a ready PR** + +Push the signed branch, create a non-draft PR with exact test and runtime evidence, wait for repository CI, and merge only if all code-owned gates pass. + +- [ ] **Step 5: Refresh NextChat #6860 evidence** + +Re-run its exact `transcribeAudio` request against the CORS-enabled server, update the PR body with the required server command and real browser-contract evidence, and route one review request to the active NextChat maintainer. From c5c9003be9e1fcb6ba420a68051f2160929f9d83 Mon Sep 17 00:00:00 2001 From: LauraGPT Date: Sun, 26 Jul 2026 20:59:39 +0000 Subject: [PATCH 3/6] feat(server): allow trusted browser origins --- funasr/bin/_server_app.py | 25 ++++++++++- tests/test_server_app_openai_segments.py | 57 ++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/funasr/bin/_server_app.py b/funasr/bin/_server_app.py index 94e2ba266..9c8a3b461 100644 --- a/funasr/bin/_server_app.py +++ b/funasr/bin/_server_app.py @@ -11,13 +11,14 @@ import logging import tempfile from pathlib import Path -from typing import Optional +from typing import Iterable, Optional import numpy as np import soundfile as sf try: from fastapi import FastAPI, UploadFile, File, Form, HTTPException + from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse except ImportError: raise ImportError( @@ -126,7 +127,13 @@ def prepare_audio_for_inference(audio_data, sr, target_sr=16000): return audio_data.astype(np.float32), sr -def create_app(device: str = "cuda", preload_model: str = "auto", model_path: str = None, hub: str = "ms") -> FastAPI: +def create_app( + device: str = "cuda", + preload_model: str = "auto", + model_path: str = None, + hub: str = "ms", + cors_origins: Optional[Iterable[str]] = None, +) -> FastAPI: if preload_model == "auto": preload_model = "fun-asr-nano" if device.startswith("cuda") else "sensevoice" @@ -138,6 +145,20 @@ def create_app(device: str = "cuda", preload_model: str = "auto", model_path: st app.state.model_path = model_path app.state.hub = hub + normalized_origins = [] + for origin in cors_origins or []: + origin = origin.strip() + if origin and origin not in normalized_origins: + normalized_origins.append(origin) + if normalized_origins: + app.add_middleware( + CORSMiddleware, + allow_origins=normalized_origins, + allow_credentials=False, + allow_methods=["GET", "POST", "OPTIONS"], + allow_headers=["Authorization", "Content-Type"], + ) + # Non-LLM model configs (use AutoModel, no vLLM) FALLBACK_CONFIGS = { "sensevoice": { diff --git a/tests/test_server_app_openai_segments.py b/tests/test_server_app_openai_segments.py index 4a735de3b..bac1316f4 100644 --- a/tests/test_server_app_openai_segments.py +++ b/tests/test_server_app_openai_segments.py @@ -18,6 +18,10 @@ def __init__(self, *args, **kwargs): self.state = types.SimpleNamespace() self.routes = {} self.metadata = kwargs + self.middleware = [] + + def add_middleware(self, middleware_class, **kwargs): + self.middleware.append((middleware_class, kwargs)) def post(self, path, *args, **kwargs): def decorator(func): @@ -39,11 +43,24 @@ def decorator(func): fastapi_stub.File = lambda *args, **kwargs: None fastapi_stub.Form = lambda *args, **kwargs: None fastapi_stub.HTTPException = Exception + fastapi_stub.__path__ = [] + + middleware_stub = types.ModuleType("fastapi.middleware") + middleware_stub.__path__ = [] + + cors_stub = types.ModuleType("fastapi.middleware.cors") + + class DummyCORSMiddleware: + pass + + cors_stub.CORSMiddleware = DummyCORSMiddleware responses_stub = types.ModuleType("fastapi.responses") responses_stub.JSONResponse = lambda content=None: content monkeypatch.setitem(sys.modules, "fastapi", fastapi_stub) + monkeypatch.setitem(sys.modules, "fastapi.middleware", middleware_stub) + monkeypatch.setitem(sys.modules, "fastapi.middleware.cors", cors_stub) monkeypatch.setitem(sys.modules, "fastapi.responses", responses_stub) module_name = "funasr_server_app_under_test" @@ -226,6 +243,46 @@ def test_server_versions_follow_package_version(monkeypatch): assert server_module.server_version_label() == f"FunASR Server v{expected}" +def test_server_cors_is_disabled_by_default(monkeypatch): + module = load_server_app(monkeypatch) + install_dummy_funasr(monkeypatch) + + app = module.create_app(device="cpu", preload_model="sensevoice") + + assert app.middleware == [] + + +def test_server_configures_normalized_trusted_origins(monkeypatch): + module = load_server_app(monkeypatch) + install_dummy_funasr(monkeypatch) + + app = module.create_app( + device="cpu", + preload_model="sensevoice", + cors_origins=[ + " http://localhost:3000 ", + "http://localhost:3000", + "http://127.0.0.1:3000", + " ", + ], + ) + + assert app.middleware == [ + ( + module.CORSMiddleware, + { + "allow_origins": [ + "http://localhost:3000", + "http://127.0.0.1:3000", + ], + "allow_credentials": False, + "allow_methods": ["GET", "POST", "OPTIONS"], + "allow_headers": ["Authorization", "Content-Type"], + }, + ) + ] + + def test_default_fun_asr_nano_uses_requested_modelscope_hub(monkeypatch): module = load_server_app(monkeypatch) DummyAutoModel = install_dummy_funasr(monkeypatch) From 7c0f43d2e4b66604c77db62d53047fb3ee896081 Mon Sep 17 00:00:00 2001 From: LauraGPT Date: Sun, 26 Jul 2026 21:01:39 +0000 Subject: [PATCH 4/6] feat(server): expose trusted CORS origins --- funasr/bin/server.py | 25 +++++++++++++++++++++--- tests/test_server_app_openai_segments.py | 18 +++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/funasr/bin/server.py b/funasr/bin/server.py index e55d75f28..2aa86f8ae 100644 --- a/funasr/bin/server.py +++ b/funasr/bin/server.py @@ -7,6 +7,7 @@ funasr-server --model paraformer funasr-server --model-path /path/to/local/model funasr-server --model-path username/paraformer --hub hf + funasr-server --cors-origin http://localhost:3000 """ import argparse @@ -21,7 +22,7 @@ def server_version_label(): return f"FunASR Server v{PACKAGE_VERSION}" -def main(): +def build_parser(): parser = argparse.ArgumentParser( description="FunASR OpenAI-Compatible API Server", formatter_class=argparse.RawDescriptionHelpFormatter, @@ -33,6 +34,7 @@ def main(): funasr-server --port 9000 # Custom port funasr-server --model-path /path/to/local/model # Use local model funasr-server --model-path username/model --hub hf # Use HuggingFace model + funasr-server --cors-origin http://localhost:3000 # Allow one browser origin Then use with OpenAI SDK: from openai import OpenAI @@ -46,7 +48,18 @@ def main(): parser.add_argument("--model", default="auto", help="Pre-load model: auto (GPU=fun-asr-nano, CPU=sensevoice), sensevoice, paraformer, fun-asr-nano") parser.add_argument("--model-path", default=None, help="Local model path or model ID (overrides --model)") parser.add_argument("--hub", default="ms", help="Model hub: ms (ModelScope), hf (HuggingFace) (default: ms)") - args = parser.parse_args() + parser.add_argument( + "--cors-origin", + action="append", + default=None, + metavar="ORIGIN", + help="Trusted browser origin for CORS; repeat for multiple origins (disabled by default)", + ) + return parser + + +def main(): + args = build_parser().parse_args() try: import uvicorn @@ -63,7 +76,13 @@ def main(): # Use inline app to avoid path issues from funasr.bin._server_app import create_app - app = create_app(device=args.device, preload_model=args.model, model_path=args.model_path, hub=args.hub) + app = create_app( + device=args.device, + preload_model=args.model, + model_path=args.model_path, + hub=args.hub, + cors_origins=args.cors_origin, + ) print(f"╔══════════════════════════════════════════════╗") print(f"║ {server_version_label():<44}║") diff --git a/tests/test_server_app_openai_segments.py b/tests/test_server_app_openai_segments.py index bac1316f4..99e90f3fb 100644 --- a/tests/test_server_app_openai_segments.py +++ b/tests/test_server_app_openai_segments.py @@ -243,6 +243,24 @@ def test_server_versions_follow_package_version(monkeypatch): assert server_module.server_version_label() == f"FunASR Server v{expected}" +def test_server_cli_collects_repeated_cors_origins(): + module = load_server_cli() + + args = module.build_parser().parse_args( + [ + "--cors-origin", + "http://localhost:3000", + "--cors-origin", + "http://127.0.0.1:3000", + ] + ) + + assert args.cors_origin == [ + "http://localhost:3000", + "http://127.0.0.1:3000", + ] + + def test_server_cors_is_disabled_by_default(monkeypatch): module = load_server_app(monkeypatch) install_dummy_funasr(monkeypatch) From 138ae21d81066502c38902c554dc0a6150d09bfe Mon Sep 17 00:00:00 2001 From: LauraGPT Date: Sun, 26 Jul 2026 21:03:20 +0000 Subject: [PATCH 5/6] docs(server): explain browser CORS setup --- docs/troubleshooting.md | 8 ++++++++ docs/troubleshooting_zh.md | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index ae17e5e29..eb2e41cd4 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -39,6 +39,14 @@ curl -X POST "http://127.0.0.1:8000/v1/audio/transcriptions" \ -F "model=FunAudioLLM/SenseVoiceSmall" ``` +- If the curl request works but a browser reports a CORS or network error, restart the server with the browser page's exact origin (scheme, host, and port): + +```bash +funasr-server --device cpu --model sensevoice \ + --cors-origin http://localhost:3000 +``` + +- Repeat `--cors-origin` for each trusted browser origin, for example when both `localhost` and `127.0.0.1` are used. Browser CORS access is disabled by default; avoid a wildcard on machines reachable by other users. - If `/v1/audio/transcriptions` returns 4xx or 5xx, attach the startup command, full server log, request command, model id, hub, and audio duration. ## WebSocket realtime output is empty or delayed diff --git a/docs/troubleshooting_zh.md b/docs/troubleshooting_zh.md index 926a4b343..1a8e52c78 100644 --- a/docs/troubleshooting_zh.md +++ b/docs/troubleshooting_zh.md @@ -39,6 +39,14 @@ curl -X POST "http://127.0.0.1:8000/v1/audio/transcriptions" \ -F "model=FunAudioLLM/SenseVoiceSmall" ``` +- 如果 curl 成功,但浏览器报 CORS 或 network error,请按浏览器页面的精确 origin(scheme、host、port)重启服务: + +```bash +funasr-server --device cpu --model sensevoice \ + --cors-origin http://localhost:3000 +``` + +- 每个可信浏览器 origin 都要重复传入一次 `--cors-origin`,例如同时使用 `localhost` 和 `127.0.0.1` 时。浏览器 CORS 默认关闭;机器可被其他用户访问时不要使用通配符。 - 如果 `/v1/audio/transcriptions` 返回 4xx 或 5xx,请附启动命令、完整 server log、请求命令、model id、hub 和音频时长。 ## WebSocket 实时输出为空或延迟很大 From 1d1457f334a147103a9ffecbfe6002eda66ebd1c Mon Sep 17 00:00:00 2001 From: LauraGPT Date: Sun, 26 Jul 2026 21:30:07 +0000 Subject: [PATCH 6/6] docs: keep CORS change user-facing --- .../plans/2026-07-27-funasr-server-cors.md | 250 ------------------ .../2026-07-27-funasr-server-cors-design.md | 84 ------ 2 files changed, 334 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-27-funasr-server-cors.md delete mode 100644 docs/superpowers/specs/2026-07-27-funasr-server-cors-design.md diff --git a/docs/superpowers/plans/2026-07-27-funasr-server-cors.md b/docs/superpowers/plans/2026-07-27-funasr-server-cors.md deleted file mode 100644 index 6ab31a3b6..000000000 --- a/docs/superpowers/plans/2026-07-27-funasr-server-cors.md +++ /dev/null @@ -1,250 +0,0 @@ -# Trusted Browser CORS Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add explicit, default-disabled trusted browser origins to `funasr-server` so local web applications can consume successful OpenAI-compatible transcription responses. - -**Architecture:** The CLI collects repeated exact origins and passes them into `create_app`. The app normalizes the list and conditionally installs FastAPI/Starlette `CORSMiddleware`; transcription routes and model loading remain unchanged. - -**Tech Stack:** Python, argparse, FastAPI, Starlette CORS middleware, pytest, curl. - -## Global Constraints - -- No CORS middleware is installed unless at least one non-empty origin is supplied. -- Allowed methods are exactly `GET`, `POST`, and `OPTIONS`. -- Allowed headers are exactly `Authorization` and `Content-Type`. -- `allow_credentials` remains `False`. -- Existing model loading and transcription response behavior must not change. -- Every behavior change follows red-green TDD. - ---- - -### Task 1: Conditional CORS Middleware - -**Files:** -- Modify: `tests/test_server_app_openai_segments.py` -- Modify: `funasr/bin/_server_app.py` - -**Interfaces:** -- Consumes: optional `cors_origins` iterable supplied by callers -- Produces: `create_app(..., cors_origins=None) -> FastAPI` with conditional middleware - -- [ ] **Step 1: Extend the FastAPI test stub and write failing tests** - -Add middleware capture to `DummyFastAPI` and stub `fastapi.middleware.cors.CORSMiddleware`: - -```python -class DummyFastAPI: - def __init__(self, *args, **kwargs): - self.state = types.SimpleNamespace() - self.routes = {} - self.metadata = kwargs - self.middleware = [] - - def add_middleware(self, middleware_class, **kwargs): - self.middleware.append((middleware_class, kwargs)) - -class DummyCORSMiddleware: - pass -``` - -Register the middleware stubs in `sys.modules`, then add behavior tests with hand-derived literal expectations: - -```python -def test_server_cors_is_disabled_by_default(monkeypatch): - module = load_server_app(monkeypatch) - install_dummy_funasr(monkeypatch) - - app = module.create_app(device="cpu", preload_model="sensevoice") - - assert app.middleware == [] - - -def test_server_configures_normalized_trusted_origins(monkeypatch): - module = load_server_app(monkeypatch) - install_dummy_funasr(monkeypatch) - - app = module.create_app( - device="cpu", - preload_model="sensevoice", - cors_origins=[ - " http://localhost:3000 ", - "http://localhost:3000", - "http://127.0.0.1:3000", - " ", - ], - ) - - assert app.middleware == [ - ( - module.CORSMiddleware, - { - "allow_origins": [ - "http://localhost:3000", - "http://127.0.0.1:3000", - ], - "allow_credentials": False, - "allow_methods": ["GET", "POST", "OPTIONS"], - "allow_headers": ["Authorization", "Content-Type"], - }, - ) - ] -``` - -- [ ] **Step 2: Run tests and verify RED** - -Run: - -```bash -python -m pytest tests/test_server_app_openai_segments.py -q -``` - -Expected: the new test fails because `create_app` does not accept `cors_origins`. - -- [ ] **Step 3: Implement the minimum middleware behavior** - -Import `CORSMiddleware`, add the optional `cors_origins` parameter, normalize with first-seen de-duplication, and call: - -```python -app.add_middleware( - CORSMiddleware, - allow_origins=normalized_origins, - allow_credentials=False, - allow_methods=["GET", "POST", "OPTIONS"], - allow_headers=["Authorization", "Content-Type"], -) -``` - -- [ ] **Step 4: Run the focused tests and verify GREEN** - -Run the same pytest command. Expected: all focused tests pass. - -- [ ] **Step 5: Commit the tested middleware** - -Stage only the two task files and create a signed commit named `feat(server): allow trusted browser origins`. - -### Task 2: Repeatable CLI Option - -**Files:** -- Modify: `tests/test_server_app_openai_segments.py` -- Modify: `funasr/bin/server.py` - -**Interfaces:** -- Consumes: repeated `--cors-origin ORIGIN` arguments -- Produces: `args.cors_origin: list[str] | None`, passed as `cors_origins=args.cors_origin` - -- [ ] **Step 1: Write a failing CLI forwarding test** - -Extract the existing parser construction into `build_parser()` and add a parser behavior test: - -```python -def test_server_cli_collects_repeated_cors_origins(): - module = load_server_cli() - - args = module.build_parser().parse_args( - [ - "--cors-origin", - "http://localhost:3000", - "--cors-origin", - "http://127.0.0.1:3000", - ] - ) - - assert args.cors_origin == [ - "http://localhost:3000", - "http://127.0.0.1:3000", - ] -``` - -- [ ] **Step 2: Run the single test and verify RED** - -Expected: argparse rejects `--cors-origin`. - -- [ ] **Step 3: Add the repeatable CLI argument** - -Move the current parser setup into `build_parser()`, use the parser from `main()`, and add: - -```python -parser.add_argument( - "--cors-origin", - action="append", - default=None, - metavar="ORIGIN", - help="Trusted browser origin for CORS; repeat for multiple origins (disabled by default)", -) -``` - -Pass the parsed list into the real application boundary: - -```python -app = create_app( - device=args.device, - preload_model=args.model, - model_path=args.model_path, - hub=args.hub, - cors_origins=args.cors_origin, -) -``` - -- [ ] **Step 4: Run focused and complete server tests** - -Expected: CLI forwarding and all server tests pass. - -- [ ] **Step 5: Commit the tested CLI surface** - -Create a signed commit named `feat(server): expose trusted CORS origins`. - -### Task 3: Operator Documentation - -**Files:** -- Modify: `docs/troubleshooting.md` -- Modify: `docs/troubleshooting_zh.md` - -**Interfaces:** -- Consumes: public `--cors-origin` CLI option -- Produces: bilingual, copy-pasteable browser deployment guidance - -- [ ] **Step 1: Add concise English and Chinese guidance** - -Document the browser CORS symptom, a trusted-origin startup command, repeated origins, and the requirement to use the browser's exact scheme/host/port. - -- [ ] **Step 2: Review the rendered Markdown contract** - -Confirm both commands use the same public CLI, both explain exact origins, neither recommends wildcard access, and neither claims CORS is enabled by default. - -- [ ] **Step 3: Run docs and server verification** - -Run the focused docs tests, all server tests, Black, Ruff, compilation, and `git diff --check`. - -- [ ] **Step 4: Commit documentation** - -Create a signed commit named `docs(server): explain browser CORS setup`. - -### Task 4: Real Browser-Contract Smoke and Publication - -**Files:** -- No production file changes unless verification exposes a tested defect. - -**Interfaces:** -- Consumes: exact feature branch head -- Produces: live preflight/transcription evidence and a reviewable FunASR PR - -- [ ] **Step 1: Start the exact branch server on CPU** - -Use SenseVoice and `--cors-origin http://127.0.0.1:3000` on an unused port. - -- [ ] **Step 2: Verify matching and non-matching origins** - -Assert matching-origin `OPTIONS` returns 200 with the expected allow-origin/method/header values, matching-origin multipart POST returns 200 with transcription text and allow-origin, and an unlisted origin receives no allow-origin header. - -- [ ] **Step 3: Verify complete repository gates** - -Run relevant tests, formatting, lint, compilation, signature checks, and diff checks at the exact head. - -- [ ] **Step 4: Push with rollback protection and open a ready PR** - -Push the signed branch, create a non-draft PR with exact test and runtime evidence, wait for repository CI, and merge only if all code-owned gates pass. - -- [ ] **Step 5: Refresh NextChat #6860 evidence** - -Re-run its exact `transcribeAudio` request against the CORS-enabled server, update the PR body with the required server command and real browser-contract evidence, and route one review request to the active NextChat maintainer. diff --git a/docs/superpowers/specs/2026-07-27-funasr-server-cors-design.md b/docs/superpowers/specs/2026-07-27-funasr-server-cors-design.md deleted file mode 100644 index 9bbd51c5d..000000000 --- a/docs/superpowers/specs/2026-07-27-funasr-server-cors-design.md +++ /dev/null @@ -1,84 +0,0 @@ -# FunASR Server Trusted Browser CORS Design - -Date: 2026-07-27 - -## Context - -Browser clients such as NextChat send OpenAI-compatible multipart transcription requests directly to a local `funasr-server`. The current server returns a valid transcription to command-line clients, but it does not return CORS headers. A browser therefore hides the successful response, and requests with an `Authorization` header fail their preflight with HTTP 405. - -## Goals - -- Let operators explicitly authorize one or more browser origins. -- Keep the current no-CORS behavior when no option is supplied. -- Support both simple multipart requests and preflighted requests with bearer tokens. -- Keep the server usable through the Python `create_app` API and the `funasr-server` CLI. -- Document a reproducible local-browser configuration in English and Chinese. - -## Non-Goals - -- Do not enable permissive CORS by default. -- Do not add authentication, cookies, an HTTP proxy, or an origin regular expression. -- Do not change transcription behavior or model loading. -- Do not make the NextChat server proxy requests to a user's local machine. - -## Interface - -The CLI gains a repeatable option: - -```bash -funasr-server \ - --device cpu \ - --model sensevoice \ - --cors-origin http://localhost:3000 \ - --cors-origin http://127.0.0.1:3000 -``` - -`create_app` gains a backward-compatible optional parameter: - -```python -def create_app( - device: str = "cuda", - preload_model: str = "auto", - model_path: str | None = None, - hub: str = "ms", - cors_origins: list[str] | None = None, -) -> FastAPI: -``` - -Empty values are ignored, surrounding whitespace is removed, and duplicate origins retain first-seen order. Passing `*` is allowed only as an explicit operator choice. - -## Middleware Policy - -When the normalized origin list is non-empty, the app adds Starlette's `CORSMiddleware` with: - -- `allow_origins`: the normalized exact origins -- `allow_methods`: `GET`, `POST`, and `OPTIONS` -- `allow_headers`: `Authorization` and `Content-Type` -- `allow_credentials`: `False` - -When the list is empty or omitted, no middleware is installed. This preserves the existing security boundary and response behavior. - -## Data Flow - -1. The operator supplies trusted browser origins on the CLI or to `create_app`. -2. The server normalizes and de-duplicates the values. -3. CORS middleware answers matching preflight requests before route dispatch. -4. The existing transcription route processes the multipart audio unchanged. -5. Middleware adds the matching `Access-Control-Allow-Origin` response header. - -## Error Handling - -An unlisted origin receives no CORS authorization header. The server still behaves normally for non-browser clients. CLI parsing remains responsible for option shape; origin reachability is not checked at startup because a valid browser origin may be offline when the service starts. - -## Verification - -- Unit tests prove default-disabled behavior and exact middleware configuration. -- CLI tests prove repeated `--cors-origin` values reach `create_app` unchanged. -- Existing server tests prove model and transcription behavior is unchanged. -- A real CPU server must return a successful matching-origin preflight and a real SenseVoice transcription with CORS headers. -- A request from an unlisted origin must not receive `Access-Control-Allow-Origin`. -- English and Chinese docs must include the explicit trusted-origin command and avoid recommending wildcard access. - -## Rollback - -The design, implementation, and documentation are separate signed commits. The feature branch is preserved remotely before merge, and the default-disabled behavior allows operators to remove the option without changing any other server configuration.