diff --git a/.env.example b/.env.example
index fdc36f2..fe0f0c8 100644
--- a/.env.example
+++ b/.env.example
@@ -31,9 +31,6 @@ VOCALIZE_HOST=127.0.0.1
VOCALIZE_PORT=8080
ORCHESTRATOR_LISTEN_PORT=8080
-# X-Invite-Token shared secret. Required when VOCALIZE_HOST != 127.0.0.1.
-VOCALIZE_INVITE_TOKEN=
-
# Public WebSocket base URL. Required when VOCALIZE_HOST != 127.0.0.1.
VOCALIZE_WS_BASE_URL=
@@ -49,6 +46,5 @@ LOG_DIR=logs
# -------------------------------------------------------------------------
# Frontend (Next.js — baked into the JS bundle at build time)
# -------------------------------------------------------------------------
-NEXT_PUBLIC_VOCALIZE_API_BASE_URL=https://vocalize-api.dgpisces.com
-NEXT_PUBLIC_VOCALIZE_WS_BASE_URL=wss://vocalize-api.dgpisces.com
-NEXT_PUBLIC_VOCALIZE_INVITE_TOKEN=
+NEXT_PUBLIC_VOCALIZE_API_BASE_URL=https://api.example.com
+NEXT_PUBLIC_VOCALIZE_WS_BASE_URL=wss://api.example.com
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index f8fd229..c10026e 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -5,9 +5,11 @@ body:
- type: markdown
attributes:
value: |
- Thank you for reporting a bug! Before submitting, please search existing
- issues to avoid duplicates. For security vulnerabilities, see
- [SECURITY.md](../../SECURITY.md) instead of filing a public issue.
+ Thank you for reporting a bug! Before submitting, please search
+ existing issues to avoid duplicates. VocalizeAI is a self-deploy
+ project — every operator runs their own backend on their own
+ infrastructure — so all reports (including security-relevant ones)
+ go through GitHub Issues here.
- type: textarea
id: what-happened
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c5037c6..4341644 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,7 +12,10 @@ VocalizeAI follows an out-of-band contribution model (D-16):
3. This keeps the private repo as the single source of truth and preserves the
`.planning/` workflow.
-If your contribution is security-related, see [SECURITY.md](SECURITY.md) first.
+VocalizeAI is a self-deploy project, so there is no centrally hosted
+instance to defend. Security-relevant fixes go through the same PR flow
+as any other contribution; flag the concern in the PR description so
+reviewers know to prioritise it.
## Setting up the dev environment
@@ -67,7 +70,7 @@ test():
refactor():
```
-Examples: `feat(server): add X-Invite-Token gate`, `fix(frontend): handle 401 on session create`.
+Examples: `feat(server): bound task length`, `fix(frontend): handle 401 on session create`.
## Branches
@@ -77,8 +80,11 @@ directly to `main`.
## Issue triage / vulnerability reporting
- Ordinary bugs and feature requests: file a GitHub issue.
-- Security vulnerabilities: follow the process in [SECURITY.md](SECURITY.md).
- Do NOT file public GitHub issues for security topics.
+- Security-relevant findings: file a GitHub issue here. VocalizeAI is a
+ self-deploy project (no centrally hosted instance), so there is no
+ separate private disclosure channel. Each operator is responsible for
+ their own deployment; report findings publicly so every operator can
+ pick up the fix.
## CI behavior for external PRs
@@ -110,9 +116,9 @@ VocalizeAI's CI pipeline has two tiers depending on where your PR originates:
VocalizeAI does not adopt a formal Code of Conduct at this stage. Standard
professional conduct is expected: be respectful, assume good faith, focus on
-the technical content. Disputes that cannot be resolved in-thread escalate to
-the maintainer via email (see SECURITY.md for the contact channel; for
-non-security disputes use the same address).
+the technical content. Disputes that cannot be resolved in-thread escalate
+by opening a GitHub issue with a clear summary; the maintainer will follow
+up there.
## License
diff --git a/README.md b/README.md
index fec8533..ed2aec1 100644
--- a/README.md
+++ b/README.md
@@ -18,16 +18,6 @@ inquiries, status checks, and more. An OSS mirror is available at
[github.com/DGPisces/VocalizeAI](https://github.com/DGPisces/VocalizeAI)
under Apache 2.0.
-## Why VocalizeAI is invite-only right now
-
-v1 uses a shared invite token (`X-Invite-Token` header on `POST /api/sessions`)
-distributed out-of-band. This is a long-lived shared secret — no rotation flow
-exists in v1; rotation requires a deploy-time env-var change plus frontend
-rebuild. Per-user authentication is v1.x scope (requirement AUTH-01).
-
-Anyone who holds the token can use the service. To request an invite token,
-email [gaodingyun2@gmail.com](mailto:gaodingyun2@gmail.com).
-
## Quick Start
**Prerequisites:** Python 3.11+, Node 20+, git, curl. Optional: `uv` (auto-installed by the script).
@@ -121,9 +111,8 @@ VocalizeAI/
| Variable | Purpose |
|----------|---------|
-| `VOCALIZE_INVITE_TOKEN` | Shared invite token for `POST /api/sessions`; required when `VOCALIZE_HOST != 127.0.0.1` |
-| `VOCALIZE_WS_BASE_URL` | WebSocket base URL returned to clients (e.g., `wss://vocalize-api.example.com`); required in non-localhost mode to prevent Host-header spoofing |
-| `VOCALIZE_CORS_ORIGINS` | Comma-separated allowed CORS origins; defaults to `https://vocalize.example.com` in non-localhost mode |
+| `VOCALIZE_WS_BASE_URL` | WebSocket base URL returned to clients (e.g., `wss://api.example.com`); required in non-localhost mode to prevent Host-header spoofing |
+| `VOCALIZE_CORS_ORIGINS` | Comma-separated allowed CORS origins; **required** in non-localhost mode (no default) |
See `.env.example` for the full env-var inventory including LLM, GPU service,
and frontend build-time variables.
@@ -205,8 +194,13 @@ follow code style, and submit contributions. Issue + PR templates live under `.g
## Security
-See [SECURITY.md](SECURITY.md) for the vulnerability reporting channel,
-threat model summary, and emergency rollback procedure.
+VocalizeAI is self-deploy: every operator runs their own backend on
+their own infrastructure, and there is no centrally hosted instance to
+defend. Report any security-relevant finding via GitHub Issues — same
+as any other bug — so every operator can pick up the fix. Self-deploy
+operators are responsible for restricting reachability at the network
+or proxy layer (Cloudflare Access, VPN, reverse-proxy auth, etc.).
+Per-user authentication is v1.x scope (requirement `AUTH-01`).
## License
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 7eb3393..570dcf1 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -15,15 +15,6 @@ clarification_collector / relay)可处理任何电话任务 —— 餐厅订
[github.com/DGPisces/VocalizeAI](https://github.com/DGPisces/VocalizeAI),
协议 Apache 2.0。
-## 为什么 VocalizeAI 当前是邀请制
-
-v1 采用共享邀请令牌机制:`POST /api/sessions` 须携带 `X-Invite-Token` 请求头,
-令牌通过线下渠道发放。这是长效共享密钥 —— v1 没有轮换流程;轮换需要修改部署环境变量
-并重新构建前端。每用户认证属于 v1.x 范畴(需求 AUTH-01)。
-
-持有令牌的人均可使用本服务。如需申请邀请令牌,请发邮件至
-[gaodingyun2@gmail.com](mailto:gaodingyun2@gmail.com)。
-
## 快速开始
**前提条件:** Python 3.11+、Node 20+、git、curl。可选:`uv`(安装脚本会自动安装)。
@@ -114,9 +105,8 @@ VocalizeAI/
| 变量 | 用途 |
|------|------|
-| `VOCALIZE_INVITE_TOKEN` | `POST /api/sessions` 所需的共享邀请令牌;`VOCALIZE_HOST != 127.0.0.1` 时必填 |
-| `VOCALIZE_WS_BASE_URL` | 返回给客户端的 WebSocket 基地址(如 `wss://vocalize-api.example.com`);非 localhost 模式必填,防止 Host 头欺骗 |
-| `VOCALIZE_CORS_ORIGINS` | 允许的 CORS 来源(逗号分隔);非 localhost 模式默认为 `https://vocalize.example.com` |
+| `VOCALIZE_WS_BASE_URL` | 返回给客户端的 WebSocket 基地址(如 `wss://api.example.com`);非 localhost 模式必填,防止 Host 头欺骗 |
+| `VOCALIZE_CORS_ORIGINS` | 允许的 CORS 来源(逗号分隔);非 localhost 模式**必填**(无默认值) |
完整环境变量清单(含 LLM、GPU 服务、前端构建变量)见 `.env.example`。
@@ -195,7 +185,11 @@ Issue 模板和 PR 模板位于 `.github/` 目录。
## 安全
-漏洞上报渠道、威胁模型摘要和紧急回滚流程,见 [SECURITY.md](SECURITY.md)。
+VocalizeAI 是自部署项目 —— 每个运维者在自己的基础设施上跑自己的后端,
+没有"统一托管实例"可被攻击。任何安全相关发现请通过 GitHub Issues 上报,
+与普通 bug 同一通道,这样每个运维者都能拿到修复。自部署运维者负责在网络/
+代理层(Cloudflare Access、VPN、反向代理认证等)限制可达性。每用户认证属于
+v1.x 范畴(需求 `AUTH-01`)。
## 许可证
diff --git a/SECURITY.md b/SECURITY.md
deleted file mode 100644
index 2df4f72..0000000
--- a/SECURITY.md
+++ /dev/null
@@ -1,97 +0,0 @@
-# Security Policy
-
-## Supported versions
-
-Only the most recent release tag receives security fixes. v1.x is
-pre-release development — fixes are applied on the main branch and
-shipped with the next release tag.
-
-## Reporting a vulnerability
-
-**Primary channel: GitHub Private Vulnerability Reporting.** Once enabled on
-the public repository (post-Phase-9 setting), open a private security advisory
-at Security → Advisories → New draft advisory. This is the preferred route — it
-keeps the report private and tracked alongside the codebase.
-
-**Backup channel: email.** Until Private Vulnerability Reporting is enabled, or
-if you cannot use it, email
-[gaodingyun2@gmail.com](mailto:gaodingyun2@gmail.com) with subject prefix
-`[VocalizeAI Security]`.
-
-Do **not** file a public GitHub issue for security topics. Disclosure
-follows responsible-disclosure norms: a private 90-day window by default,
-with earlier coordinated release if an active exploit is confirmed.
-
-Please include:
-- Description of the vulnerability and affected component
-- Steps to reproduce
-- Potential impact assessment
-
-## Threat model summary
-
-### Access posture
-
-v1 uses a shared invite-token model. `POST /api/sessions` requires an
-`X-Invite-Token` header whose value must match `VOCALIZE_INVITE_TOKEN`
-configured server-side. This is an intentional design for v1 (requirement
-`AUTH-DEFER-TO-v1.x`); per-user authentication is v1.x scope (requirement
-`AUTH-01`).
-
-### Network layer
-
-- TLS termination at the Cloudflare edge via Universal SSL.
-- A `cloudflared` tunnel connects the Pi orchestrator to the Cloudflare
- edge; all external traffic enters through Cloudflare before reaching
- the backend.
-
-### Backend security controls
-
-- **CORS**: single allowed origin (`https://vocalize.dgpisces.com`) in
- production; localhost origins preserved in dev mode via
- `VOCALIZE_HOST` env-conditional config. `allow_methods` restricted to
- `["GET", "POST", "DELETE"]`.
-- **WS base URL enforcement**: server raises at startup if
- `VOCALIZE_HOST != "127.0.0.1"` and `VOCALIZE_WS_BASE_URL` is unset,
- preventing Host-header spoofing.
-- **Task length bound**: `SetTaskRequest.task` has a `max_length=2000`
- field constraint to limit prompt-injection surface area.
-
-## Known limitations
-
-### Shared invite token in JS bundle
-
-`NEXT_PUBLIC_VOCALIZE_INVITE_TOKEN` is embedded in the Next.js JS bundle
-at build time by design — this is inherent to the shared-token model.
-It is not a vulnerability; the token IS the gate. Anyone who can load the
-page already has the token. Per-user secrets are v1.x scope (AUTH-01).
-
-### No token rotation flow in v1
-
-The invite token is a long-lived shared secret. Rotation requires a
-deploy-time env-var update plus a frontend rebuild. There is no in-band
-rotation API in v1.
-
-### No prompt-injection mitigation
-
-User-supplied task descriptions are passed to the LLM without sanitization.
-This is a known gap flagged for v1.x scope.
-
-## Emergency rollback for leaked secret in public mirror
-
-If a secret (invite token, API key, tunnel ID) is accidentally committed
-to the public repo:
-
-1. **Rotate the leaked secret immediately.**
- - `VOCALIZE_INVITE_TOKEN`: update Pi `.env`, restart `vocalize` service,
- rebuild frontend with the new `NEXT_PUBLIC_VOCALIZE_INVITE_TOKEN`.
- - Other secrets: follow their respective rotation procedures.
-
-2. **Force-push the public `main`** to a re-sanitized commit by re-running
- the `sync-private-to-public` skill against a clean private tree. This
- overwrites the public history and removes the leaked commit.
-
-3. **Contact GitHub Support** to purge cached references:
- https://support.github.com/contact/github-private-information-removal
-
-4. **Audit the blast radius**: determine whether the secret was used by
- any unauthorized party before rotation.
diff --git a/docs/architecture.md b/docs/architecture.md
index f33b82d..b080669 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -28,7 +28,7 @@ the AI audio pipeline and the PSTN call.
### End-to-End Request Flow (Quick Reference)
-1. **User creates a session** — `POST /api/sessions` with `X-Invite-Token` → receives
+1. **User creates a session** — `POST /api/sessions` → receives
`session_id` + `ws_url`.
2. **User sets the task** — `POST /api/sessions/{id}/task` with `{"task": "..."}` → Phase
transitions `draft → task_planning → collecting`.
@@ -374,16 +374,17 @@ See: `src/vocalize/server/frames.py`
## REST Surface
-The REST API is mounted at `/api/sessions`. Authentication is via a shared
-invite token (`X-Invite-Token` header on session creation). In localhost-dev
-mode (`VOCALIZE_HOST=127.0.0.1` and `VOCALIZE_INVITE_TOKEN` unset), the token
-gate is disabled.
+The REST API is mounted at `/api/sessions`. The backend ships no
+request-level authentication in v1; self-deploy operators restrict
+reachability at the network or proxy layer (per-user auth is v1.x
+scope — requirement `AUTH-01`).
See: `src/vocalize/server/sessions.py`, `src/vocalize/server/health.py`
### `POST /api/sessions`
-**Auth:** `X-Invite-Token` header (design constraint D-08; uses `secrets.compare_digest`)
+**Auth:** None at the backend layer in v1 (network/proxy restriction is the
+operator's responsibility).
**Request body** (`CreateSessionRequest`, all optional):
```json
@@ -405,7 +406,7 @@ See: `src/vocalize/server/sessions.py`, `src/vocalize/server/health.py`
}
```
-See: `src/vocalize/server/sessions.py` — `_check_invite_token`, `CreateSessionRequest`
+See: `src/vocalize/server/sessions.py` — `CreateSessionRequest`
---
@@ -565,17 +566,17 @@ See: `src/vocalize/server/ws.py`, `frontend/lib/audio*`, `frontend/components/Br
## Security Posture
The security controls relevant to the architecture are documented here for
-API consumers and security researchers. For the full threat model and disclosure
-channel, see [SECURITY.md](../SECURITY.md).
+API consumers and security researchers. VocalizeAI is a self-deploy
+project (no centrally hosted instance); report security-relevant findings
+via GitHub Issues — same channel as any other bug.
-### Invite-Token Gate (D-08)
+### Authentication (D-08, retired)
-`POST /api/sessions` requires `X-Invite-Token: ` matching the server-side
-`VOCALIZE_INVITE_TOKEN` env var. The comparison uses `secrets.compare_digest` to
-prevent timing attacks. In localhost-dev mode (`VOCALIZE_HOST=127.0.0.1` and
-`VOCALIZE_INVITE_TOKEN` unset), the gate is disabled for development convenience.
-
-See: `src/vocalize/server/sessions.py` — `_check_invite_token`
+The original D-08 shared-invite-token gate has been removed; v1 ships no
+backend-level auth on `POST /api/sessions` or the WebSocket. Self-deploy
+operators are expected to restrict reachability at the network or proxy
+layer (Cloudflare Access, VPN, reverse-proxy auth, etc.). Per-user
+authentication is v1.x scope (requirement `AUTH-01`).
### Task Length Bound (D-09)
@@ -612,4 +613,3 @@ See: `src/vocalize/server/ws.py`, `src/vocalize/server/sessions.py`
- **[docs/deploy/local.md](docs/deploy/local.md)** — Mac/Linux dev environment setup and env-var reference
- **[docs/deploy/pi.md](docs/deploy/pi.md)** — End-to-end Pi production deployment runbook
- **[CONTRIBUTING.md](../CONTRIBUTING.md)** — Contributor flow, code style, commit conventions
-- **[SECURITY.md](../SECURITY.md)** — Vulnerability reporting, threat model, emergency rollback
diff --git a/docs/deploy/local.md b/docs/deploy/local.md
index 3181281..631d5fb 100644
--- a/docs/deploy/local.md
+++ b/docs/deploy/local.md
@@ -78,19 +78,18 @@ $EDITOR .env
| `VOCALIZE_HOST` | default ok | uvicorn bind host; `127.0.0.1` for local dev, `0.0.0.0` for production |
| `VOCALIZE_PORT` | default ok | uvicorn bind port; default `8080` (note: dev `main.py` defaults to 8000) |
| `ORCHESTRATOR_LISTEN_PORT` | default ok | Pi service port; default `8080` (legacy; mirrors `VOCALIZE_PORT`) |
-| `VOCALIZE_INVITE_TOKEN` | required when non-localhost | Shared invite secret for `POST /api/sessions`; **gate is disabled when `VOCALIZE_HOST=127.0.0.1` and this var is unset** — localhost-dev shortcut |
-| `VOCALIZE_WS_BASE_URL` | required when non-localhost | Public WS base URL (e.g. `wss://vocalize-api.example.com`); startup raises if missing in non-localhost mode (D-11) |
+| `VOCALIZE_WS_BASE_URL` | required when non-localhost | Public WS base URL (e.g. `wss://api.example.com`); startup raises if missing in non-localhost mode (D-11) |
| `VOCALIZE_CORS_ORIGINS` | default ok | Comma-separated allowed CORS origins; auto-picked from VOCALIZE_HOST in dev mode |
| `DEFAULT_LANGUAGE` | default ok | Session default language; `zh` or `en`; default `zh` |
| `LOG_DIR` | default ok | Log directory; default `logs` |
| `NEXT_PUBLIC_VOCALIZE_API_BASE_URL` | yes for frontend | Frontend API base URL baked into the Next.js JS bundle at build time |
| `NEXT_PUBLIC_VOCALIZE_WS_BASE_URL` | optional | Frontend WS base; derived from `NEXT_PUBLIC_VOCALIZE_API_BASE_URL` if absent |
-| `NEXT_PUBLIC_VOCALIZE_INVITE_TOKEN` | yes for frontend in prod | Invite token baked into Next.js JS bundle; required for the frontend to authenticate session creation |
-**Localhost-dev shortcut:** when `VOCALIZE_HOST` is `127.0.0.1` and
-`VOCALIZE_INVITE_TOKEN` is unset, the `X-Invite-Token` gate on `POST /api/sessions`
-is disabled. This means you can `curl http://127.0.0.1:8000/api/sessions` without
-supplying a token — convenient for local development.
+**Backend auth posture:** v1 ships no request-level auth on
+`POST /api/sessions` or the WebSocket. For non-localhost deployments,
+restrict reachability at the network or proxy layer (Cloudflare Access,
+VPN, reverse-proxy auth, etc.). Per-user auth is v1.x scope
+(requirement `AUTH-01`).
**Minimum for local dev (no GPU):**
```bash
@@ -156,8 +155,7 @@ Exit code 0 = the development environment is working. The smoke script exercises
6 round-trips: health check, create session, set task, WS upgrade + send/recv,
delete session. Total runtime is ~20 seconds.
-The smoke script uses `VOCALIZE_API_BASE` (default `http://127.0.0.1:8000`) and
-`VOCALIZE_INVITE_TOKEN` (default empty, gate disabled in localhost-dev mode).
+The smoke script uses `VOCALIZE_API_BASE` (default `http://127.0.0.1:8000`).
---
diff --git a/docs/deploy/pi.md b/docs/deploy/pi.md
index 904d501..1a56e20 100644
--- a/docs/deploy/pi.md
+++ b/docs/deploy/pi.md
@@ -127,14 +127,12 @@ sudo nano /opt/vocalize/.env
| `VOCALIZE_HOST` | default ok | uvicorn bind host; set to `0.0.0.0` for Pi production |
| `VOCALIZE_PORT` | default ok | uvicorn bind port; default `8080` |
| `ORCHESTRATOR_LISTEN_PORT` | default ok | Pi service port; default `8080` (legacy compatibility) |
-| `VOCALIZE_INVITE_TOKEN` | **yes** | Shared invite secret for `POST /api/sessions`; generate with `python3 -c 'import secrets; print(secrets.token_urlsafe(32))'` |
-| `VOCALIZE_WS_BASE_URL` | **yes** | Public WS base URL; e.g. `wss://vocalize-api.` — startup raises if missing in non-localhost mode |
+| `VOCALIZE_WS_BASE_URL` | **yes** | Public WS base URL; e.g. `wss://api.` — startup raises if missing in non-localhost mode |
| `VOCALIZE_CORS_ORIGINS` | default ok | Comma-separated allowed CORS origins; default auto-picked from VOCALIZE_HOST |
| `DEFAULT_LANGUAGE` | default ok | `zh` or `en`; default `zh` |
| `LOG_DIR` | default ok | Log directory; default `logs` |
| `NEXT_PUBLIC_VOCALIZE_API_BASE_URL` | yes (for frontend) | Frontend API base URL; baked into JS bundle at build time |
| `NEXT_PUBLIC_VOCALIZE_WS_BASE_URL` | optional | Frontend WS base; derived from API base if absent |
-| `NEXT_PUBLIC_VOCALIZE_INVITE_TOKEN` | yes (for frontend in prod) | Shared invite token baked into JS bundle |
**Example production `.env` (use your own values for all `<...>` placeholders):**
@@ -147,9 +145,8 @@ SENSEVOICE_WS_PORT=8000
COSYVOICE_WS_PORT=8001
VOCALIZE_HOST=0.0.0.0
VOCALIZE_PORT=8080
-VOCALIZE_INVITE_TOKEN=
-VOCALIZE_WS_BASE_URL=wss://vocalize-api.
-VOCALIZE_CORS_ORIGINS=https://vocalize.
+VOCALIZE_WS_BASE_URL=wss://api.
+VOCALIZE_CORS_ORIGINS=https://
```
---
@@ -248,9 +245,7 @@ VOCALIZE_API_BASE=http://127.0.0.1:8080 bash scripts/smoke.sh
# Exit 0 = working deployment
# Smoke test against the public Cloudflare Tunnel URL:
-VOCALIZE_API_BASE=https://vocalize-api. \
- VOCALIZE_INVITE_TOKEN= \
- bash scripts/smoke.sh
+VOCALIZE_API_BASE=https://api. bash scripts/smoke.sh
```
The smoke script exercises 6 round-trips: `GET /health`, `POST /api/sessions`,
diff --git a/docs/release/24h-stability-evidence.md b/docs/release/24h-stability-evidence.md
index bb893e6..4d7a34d 100644
--- a/docs/release/24h-stability-evidence.md
+++ b/docs/release/24h-stability-evidence.md
@@ -40,7 +40,7 @@ closes this gap:
| Operator | DGPisces |
| Date / time (start) | 2026-05-17T19:56:20Z |
| Date / time (end) | 2026-05-18T19:56:20Z |
-| Backend URL | `http://localhost:8080` (Pi loopback; same orchestrator that `https://vocalize-api.dgpisces.com` fronts via Cloudflare Tunnel) |
+| Backend URL | `http://localhost:8080` (Pi loopback; same orchestrator that `https://api.example.com` fronts via Cloudflare Tunnel) |
| Browser and version | N/A — driver-only run; real-audio interleave intentionally skipped (see Real-Audio Interleave Log below) |
| STT service health | Pass (`/health` reported `gpu_reachable: true` throughout) |
| TTS service health | Pass (CosyVoice docker container up 2 days, healthy) |
@@ -87,7 +87,6 @@ transient failures acceptable).
Driver command used:
```bash
export VOCALIZE_API_BASE=http://localhost:8080
-export VOCALIZE_INVITE_TOKEN=
python scripts/stability-24h-driver.py \
--duration-minutes 1440 \
--scenario balance_inquiry_en_query \
@@ -95,7 +94,7 @@ python scripts/stability-24h-driver.py \
--evidence-out docs/release/24h-stability-evidence-runs/2026-05-17T195620Z-pi-loopback.md
```
-Driver invoked with API base = `http://localhost:8080` (Pi loopback) rather than `https://vocalize-api.dgpisces.com` because the driver ran on the Pi itself per operator preference (full automation, no Attu host). The Cloudflare Tunnel ingress for `vocalize.dgpisces.com` was verified live separately during DEPLOY-03 deploy (curl → HTTP/2 502 + Universal SSL `*.dgpisces.com` cert).
+Driver invoked with API base = `http://localhost:8080` (Pi loopback) rather than `https://api.example.com` because the driver ran on the Pi itself per operator preference (full automation, no Attu host). The Cloudflare Tunnel ingress for `vocalize.example.com` was verified live separately during DEPLOY-03 deploy (curl → HTTP/2 502 + Universal SSL `*.example.com` cert).
---
diff --git a/frontend/app/components.css b/frontend/app/components.css
index 62d9a41..1ea7733 100644
--- a/frontend/app/components.css
+++ b/frontend/app/components.css
@@ -6,7 +6,7 @@
* except where a value is component-shape (e.g. timer-bar height) and
* expressed in design-system spacing/timing units.
*
- * Visual character: dgpisces design system (Apple-style; SF font stack;
+ * Visual character: shared design system (Apple-style; SF font stack;
* 12px radius cards, 8px sm radius, pill radius for chips). Bubbles and
* speaker-attributed surfaces follow conversation-app conventions while
* keeping a refined, minimal tone consistent with the existing B2 chrome.
diff --git a/frontend/app/design-tokens.css b/frontend/app/design-tokens.css
index 28de729..76ab096 100644
--- a/frontend/app/design-tokens.css
+++ b/frontend/app/design-tokens.css
@@ -1,5 +1,5 @@
/*
- * dgpisces design system — token primitives (web platform).
+ * shared design system — token primitives (web platform).
*
* Source: ~/Claude Code/Web Dev/design-system/platforms/web/_tokens.css
* (mirror of tokens/tokens.json v1.3.0). Vendored here because the design
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
index 1316f72..268b185 100644
--- a/frontend/app/globals.css
+++ b/frontend/app/globals.css
@@ -1,7 +1,7 @@
/*
* globals.css — base resets + B2 (browser audio bridge) chrome.
*
- * Token primitives live in design-tokens.css (vendored from the dgpisces
+ * Token primitives live in design-tokens.css (vendored from the shared
* design system). Component classes for B3a phases live in components.css.
* Keep this file scoped to: design-token import, base resets, body chrome,
* skip-link, and the B2 browser-audio shell (.app-shell / .page-frame /
diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts
index 42d5183..8537e01 100644
--- a/frontend/lib/api.ts
+++ b/frontend/lib/api.ts
@@ -53,34 +53,10 @@ function apiBaseUrl(): string {
return value.replace(/\/$/, "");
}
-/**
- * Returns the invite token from the build-time env var, or null when unset.
- *
- * Returning null (not throwing) is intentional: localhost-dev must work without
- * any token configured.
- *
- * WARNING: NEXT_PUBLIC_ vars are embedded in the client-side JS bundle and are
- * therefore recoverable by any user who can load the page. This is an accepted
- * v1 limitation — the shared token provides friction, not true access control.
- * Per-user auth (AUTH-01, v1.x) will replace this gate entirely.
- *
- * For stronger protection before AUTH-01 lands, consider routing the session
- * creation call through a Next.js API route that reads the token from a
- * server-side env var (no NEXT_PUBLIC_ prefix) so it never reaches the bundle.
- */
-function inviteToken(): string | null {
- const value = process.env.NEXT_PUBLIC_VOCALIZE_INVITE_TOKEN;
- return value && value.length > 0 ? value : null;
-}
-
export async function createSession(
body: CreateSessionRequest = {},
): Promise {
const headers: Record = { "Content-Type": "application/json" };
- const token = inviteToken();
- if (token !== null) {
- headers["X-Invite-Token"] = token;
- }
const res = await fetch(`${apiBaseUrl()}/api/sessions`, {
method: "POST",
cache: "no-store",
@@ -88,7 +64,6 @@ export async function createSession(
body: JSON.stringify(body),
});
if (!res.ok) {
- // 401 = invite-required per D-08; bubble to caller for user-facing "invite required" message
throw new Error(`createSession failed: ${res.status}`);
}
return (await res.json()) as SessionResponse;
diff --git a/frontend/scripts/generate-og.py b/frontend/scripts/generate-og.py
index 55cd608..ed5ba04 100644
--- a/frontend/scripts/generate-og.py
+++ b/frontend/scripts/generate-og.py
@@ -5,7 +5,7 @@
frontend/public/og/og-zh.png (1200×630, Chinese)
frontend/public/og/og-en.png (1200×630, English)
-Design system: dgpisces Apple-style tokens
+Design system: Apple-style design tokens
- Accent: #007aff (blue) / #5e5ce6 (purple)
- BG: linear gradient from #0a0a0f → #1a1a2e
- Text: #f5f5f7 (primary), #98989d (soft)
@@ -312,7 +312,7 @@ def generate(lang: str, tagline: str, out_path: str):
font_url = ImageFont.truetype(FONT_MONO, 15)
except Exception:
font_url = ImageFont.load_default()
- draw.text((tag_x, strip_y), "vocalize.dgpisces.com", font=font_url, fill=TEXT_SOFT)
+ draw.text((tag_x, strip_y), "vocalize.example.com", font=font_url, fill=TEXT_SOFT)
# 10. Decorative divider line (vertical, separating text from illustration)
div_x = int(W * 0.60)
diff --git a/infra/pi-orchestrator/.env.template b/infra/pi-orchestrator/.env.template
index 6ca1163..dcf758e 100644
--- a/infra/pi-orchestrator/.env.template
+++ b/infra/pi-orchestrator/.env.template
@@ -8,15 +8,10 @@ VOCALIZE_PORT=8080
# Required for non-localhost deployments — see D-11.
# If unset on non-localhost host, create_app() raises RuntimeError and systemd will restart-loop.
-VOCALIZE_WS_BASE_URL=wss://vocalize-api.dgpisces.com
-
-# Required for the X-Invite-Token gate on POST /api/sessions — see D-08.
-# Generate with: python3 -c 'import secrets; print(secrets.token_urlsafe(32))'
-# MUST be set; an empty value disables the gate, exposing session creation publicly.
-VOCALIZE_INVITE_TOKEN=
+VOCALIZE_WS_BASE_URL=wss://api.example.com
# Production CORS — see D-10. Pin to single origin in prod.
-VOCALIZE_CORS_ORIGINS=https://vocalize.dgpisces.com
+VOCALIZE_CORS_ORIGINS=https://vocalize.example.com
# Test-only: enables merchant_text_inject frame for AI-merchant scenarios.
# DO NOT set this outside the 24h evidence run.
diff --git a/infra/pi-orchestrator/cloudflared-config.yml b/infra/pi-orchestrator/cloudflared-config.yml
index 8982b47..92f4bbc 100644
--- a/infra/pi-orchestrator/cloudflared-config.yml
+++ b/infra/pi-orchestrator/cloudflared-config.yml
@@ -7,16 +7,16 @@
#
# This file documents the intended ingress shape so reviewers can see what the
# tunnel exposes; the actual ingress lives in the Cloudflare dashboard
-# (Zero Trust → Networks → Tunnels → dgpisces-server1 → Public Hostname) /
+# (Zero Trust → Networks → Tunnels → your-tunnel-name → Public Hostname) /
# Cloudflare API tunnel config. The dashboard / API is the source of truth —
# update this YAML whenever the live ingress changes so reviewers of the OSS
# mirror see the current shape.
#
-# Tunnel: dgpisces-server1 ()
+# Tunnel: your-tunnel-name ()
ingress:
- - hostname: vocalize-api.dgpisces.com
+ - hostname: api.example.com
service: http://localhost:8080
- - hostname: vocalize.dgpisces.com
+ - hostname: vocalize.example.com
service: http://localhost:3000
- service: http_status:404
diff --git a/infra/pi-orchestrator/setup.sh b/infra/pi-orchestrator/setup.sh
index b23d110..99146cd 100755
--- a/infra/pi-orchestrator/setup.sh
+++ b/infra/pi-orchestrator/setup.sh
@@ -88,7 +88,7 @@ echo " ssh ${PI_USER}@${PI_HOST} 'nano ${PI_PROJECT_DIR}/.env'"
echo ""
echo "2. Install cloudflared service on Pi using a tunnel token (token-based auth):"
echo " a. Get the connector token from the Cloudflare dashboard:"
-echo " Zero Trust → Networks → Tunnels → dgpisces-server1 → Configure"
+echo " Zero Trust → Networks → Tunnels → your-tunnel-name → Configure"
echo " → Install and run a connector → copy the long token string"
echo " b. SSH to Pi and install the service with that token:"
echo " ssh ${PI_USER}@${PI_HOST} 'sudo cloudflared service install '"
@@ -97,13 +97,13 @@ echo " config.yml is needed. (If you ever want to inspect intended ingress,
echo " infra/pi-orchestrator/cloudflared-config.yml; that file is reference-only.)"
echo ""
echo "3. Make sure Public Hostname routing is configured in the dashboard:"
-echo " vocalize-api.dgpisces.com → http://localhost:8080"
+echo " api.example.com → http://localhost:8080"
echo ""
echo "4. Start services:"
echo " ssh ${PI_USER}@${PI_HOST} 'sudo systemctl start vocalize cloudflared'"
echo ""
echo "5. Verify both services are running and the public URL responds:"
echo " ssh ${PI_USER}@${PI_HOST} 'sudo systemctl status vocalize cloudflared --no-pager'"
-echo " curl -fsS https://vocalize-api.dgpisces.com/health"
+echo " curl -fsS https://api.example.com/health"
echo ""
echo "After completing manual steps, use deploy.sh for subsequent deploys."
diff --git a/install/pi-install.sh b/install/pi-install.sh
index 07b2686..511c54b 100755
--- a/install/pi-install.sh
+++ b/install/pi-install.sh
@@ -197,7 +197,7 @@ step6_systemd_unit() {
run_or_dry "copy .env.template -> ${INSTALL_DIR}/.env" \
sudo cp "${ENV_SRC}" "${ENV_DST}"
echo " Created ${ENV_DST} from template — edit it before starting the service."
- echo " At minimum, set: OPENAI_API_KEY, VOCALIZE_INVITE_TOKEN, VOCALIZE_WS_BASE_URL, GPU_HOST"
+ echo " At minimum, set: OPENAI_API_KEY, VOCALIZE_WS_BASE_URL, VOCALIZE_CORS_ORIGINS, GPU_HOST"
else
echo " ${ENV_DST} already exists — preserving."
fi
diff --git a/install/public-allowlist.md b/install/public-allowlist.md
index 8664e33..90eae26 100644
--- a/install/public-allowlist.md
+++ b/install/public-allowlist.md
@@ -15,7 +15,6 @@ Paths are anchored at repo root. Used by `scripts/build-public-filelist.py`.
- CHANGELOG.md
- CODEOWNERS
- LICENSE
-- SECURITY.md
- CONTRIBUTING.md
- pyproject.toml
- .env.example
diff --git a/pyproject.toml b/pyproject.toml
index 20d88b7..77394c5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
name = "vocalize-ai"
version = "0.1.0"
description = "Bilingual (zh/en) universal phone-task AI agent"
-authors = [{name = "DGPisces", email = "gaodingyun2@gmail.com"}]
+authors = [{name = "DGPisces"}]
license = "Apache-2.0"
readme = "README.md"
requires-python = ">=3.11"
diff --git a/scripts/smoke.sh b/scripts/smoke.sh
index fa8c4a3..e5cf46e 100755
--- a/scripts/smoke.sh
+++ b/scripts/smoke.sh
@@ -15,14 +15,13 @@ IFS=$'\n\t'
# Usage:
# bash scripts/smoke.sh
# VOCALIZE_API_BASE=http://127.0.0.1:8080 bash scripts/smoke.sh
-# VOCALIZE_INVITE_TOKEN= VOCALIZE_API_BASE=https://api.example.com bash scripts/smoke.sh
+# VOCALIZE_API_BASE=https://api.example.com bash scripts/smoke.sh
#
# Exits 0 on all-pass; exits 1 with descriptive message on any failure.
# Total runtime budget: ~20 seconds (WS step has a 10s timeout).
# ---------------------------------------------------------------------------
BASE="${VOCALIZE_API_BASE:-http://127.0.0.1:8000}"
-TOKEN="${VOCALIZE_INVITE_TOKEN:-}"
SMOKE_START=$(date +%s)
SID=""
@@ -81,10 +80,9 @@ echo ""
echo "[2/5] POST ${BASE}/api/sessions..."
SESSIONS_RESP=$(curl -fsS --max-time 10 -X POST "${BASE}/api/sessions" \
- -H "X-Invite-Token: ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"default_lang":"zh","auto_translate_merchant":true}' 2>/dev/null) \
- || fail "[2/5]" "curl failed — is the backend running and invite token correct?"
+ || fail "[2/5]" "curl failed — is the backend running?"
SID=$(echo "$SESSIONS_RESP" | jq -r '.session_id' 2>/dev/null) \
|| fail "[2/5]" "response is not valid JSON: ${SESSIONS_RESP}"
diff --git a/scripts/stability-24h-driver.py b/scripts/stability-24h-driver.py
index 1692c53..a0dec12 100644
--- a/scripts/stability-24h-driver.py
+++ b/scripts/stability-24h-driver.py
@@ -7,20 +7,16 @@
Usage (run from a remote runner host):
ssh
cd /path/to/VocalizeAI
- export VOCALIZE_API_BASE=https://vocalize-api.dgpisces.com
- export VOCALIZE_INVITE_TOKEN=
+ export VOCALIZE_API_BASE=https://api.example.com
python scripts/stability-24h-driver.py --duration-minutes 1440 \\
--scenario balance_inquiry_en_query --seed direct
Environment variables:
VOCALIZE_API_BASE Base URL for the Pi orchestrator REST API.
- Default: https://vocalize-api.dgpisces.com
- VOCALIZE_INVITE_TOKEN Required when API base is non-localhost.
- Set to match VOCALIZE_INVITE_TOKEN on the Pi.
+ Default: https://api.example.com
Prerequisites on Pi (MUST be set before the 24h run):
VOCALIZE_ENABLE_TEST_FRAMES=1 in /opt/vocalize/.env (or your VOCALIZE_HOME path)
- VOCALIZE_INVITE_TOKEN= in /opt/vocalize/.env (or your VOCALIZE_HOME path)
After the run, paste the output file into
docs/release/24h-stability-evidence.md for the release record.
@@ -53,7 +49,7 @@
DEFAULT_DURATION_MIN = 24 * 60 # 24 hours
DEFAULT_SCENARIO_ID = "balance_inquiry_en_query"
DEFAULT_SEED_ID = "direct"
-DEFAULT_API_BASE = "https://vocalize-api.dgpisces.com"
+DEFAULT_API_BASE = "https://api.example.com"
# Path to scenarios.yaml (sibling of tests/integration/ai_merchant.py)
_SCENARIOS_YAML = (
@@ -118,7 +114,6 @@ def _get_merchant_lang(scenario_id: str) -> str:
# ---------------------------------------------------------------------------
async def one_cycle(
api_base: str,
- invite_token: str | None,
scenario_id: str,
seed_id: str,
) -> dict[str, Any]:
@@ -131,8 +126,6 @@ async def one_cycle(
"""
cycle_start = time.time()
headers: dict[str, str] = {}
- if invite_token:
- headers["X-Invite-Token"] = invite_token
user_lang = _get_user_lang(scenario_id)
merchant_lang = _get_merchant_lang(scenario_id)
@@ -263,14 +256,9 @@ async def one_cycle(
# ---------------------------------------------------------------------------
# Metrics snapshot
# ---------------------------------------------------------------------------
-async def fetch_metrics_snapshot(
- api_base: str,
- invite_token: str | None = None,
-) -> str:
+async def fetch_metrics_snapshot(api_base: str) -> str:
"""GET {api_base}/metrics and return the Prometheus exposition text."""
headers: dict[str, str] = {}
- if invite_token:
- headers["X-Invite-Token"] = invite_token
try:
async with httpx.AsyncClient(timeout=15.0) as client:
resp = await client.get(f"{api_base}/metrics", headers=headers)
@@ -370,16 +358,6 @@ def _write_evidence(
# ---------------------------------------------------------------------------
async def main(args: argparse.Namespace) -> None:
api_base = args.api_base
- invite_token = args.invite_token
-
- # Require token for non-localhost
- is_localhost = api_base.startswith("http://127.") or api_base.startswith("http://localhost")
- if not is_localhost and not invite_token:
- log.error(
- "VOCALIZE_INVITE_TOKEN is required when VOCALIZE_API_BASE is not localhost.\n"
- "Set it in your environment before running the driver."
- )
- sys.exit(1)
log.info(
"stability-24h-driver starting: api_base=%s scenario=%s seed=%s duration_min=%d",
@@ -393,7 +371,7 @@ async def main(args: argparse.Namespace) -> None:
metrics_snapshots: list[tuple[float, str]] = []
# Initial metrics snapshot
- snap = await fetch_metrics_snapshot(api_base, invite_token)
+ snap = await fetch_metrics_snapshot(api_base)
metrics_snapshots.append((time.time(), snap))
log.info("initial /metrics snapshot taken")
@@ -403,7 +381,7 @@ async def main(args: argparse.Namespace) -> None:
cycle_start = time.time()
log.info("--- cycle %d start (remaining: %.0f min) ---", cycle_num, (end - cycle_start) / 60)
- result = await one_cycle(api_base, invite_token, args.scenario, args.seed)
+ result = await one_cycle(api_base, args.scenario, args.seed)
session_log.append(result)
status = "OK" if result["ok"] else f"FAIL: {result.get('error', '')}"
log.info("cycle %d result: %s (%.1fs)", cycle_num, status, result.get("elapsed_s", 0))
@@ -411,7 +389,7 @@ async def main(args: argparse.Namespace) -> None:
# Periodic metrics snapshot
last_snap_t = metrics_snapshots[-1][0]
if (cycle_start - last_snap_t) >= args.metrics_snapshot_interval_hours * 3600:
- snap = await fetch_metrics_snapshot(api_base, invite_token)
+ snap = await fetch_metrics_snapshot(api_base)
metrics_snapshots.append((time.time(), snap))
log.info("metrics snapshot taken at cycle %d", cycle_num)
@@ -423,7 +401,7 @@ async def main(args: argparse.Namespace) -> None:
await asyncio.sleep(min(sleep_s, remaining))
# Final metrics snapshot
- snap = await fetch_metrics_snapshot(api_base, invite_token)
+ snap = await fetch_metrics_snapshot(api_base)
metrics_snapshots.append((time.time(), snap))
log.info("final /metrics snapshot taken; %d cycles completed", cycle_num)
@@ -484,7 +462,6 @@ def _build_arg_parser() -> argparse.ArgumentParser:
# Resolve env vars
args.api_base = os.environ.get("VOCALIZE_API_BASE", DEFAULT_API_BASE).rstrip("/")
- args.invite_token = os.environ.get("VOCALIZE_INVITE_TOKEN") or None
# Default evidence output path
if args.evidence_out is None:
diff --git a/src/vocalize/config.py b/src/vocalize/config.py
index ae75d7e..efccbed 100644
--- a/src/vocalize/config.py
+++ b/src/vocalize/config.py
@@ -53,9 +53,6 @@ class Config:
# 日志配置
log_dir: str = "logs"
- # Phase 4 D-08: X-Invite-Token 共享密钥;None = 本地开发模式,跳过邀请验证
- invite_token: str | None = None
-
@classmethod
def from_env(cls) -> "Config":
"""从环境变量和 .env 文件加载配置。"""
@@ -74,7 +71,6 @@ def from_env(cls) -> "Config":
),
default_language=os.getenv("DEFAULT_LANGUAGE", cls.default_language),
log_dir=os.getenv("LOG_DIR", cls.log_dir),
- invite_token=os.getenv("VOCALIZE_INVITE_TOKEN") or None,
)
def validate_for_phase(
diff --git a/src/vocalize/server/__init__.py b/src/vocalize/server/__init__.py
index 4396792..8e0de69 100644
--- a/src/vocalize/server/__init__.py
+++ b/src/vocalize/server/__init__.py
@@ -47,7 +47,7 @@ def _default_user_pipeline_factory(transport):
)
-_DEFAULT_PROD_ORIGINS = ["https://vocalize.dgpisces.com"]
+_DEFAULT_PROD_ORIGINS: list[str] = []
_DEFAULT_DEV_ORIGINS = ["http://localhost:3000", "http://127.0.0.1:3000"]
@@ -58,14 +58,12 @@ def create_app() -> FastAPI:
VOCALIZE_HOST / VOCALIZE_PORT — where uvicorn binds (handled by main()).
VOCALIZE_CORS_ORIGINS — comma-separated allowed origins; defaults to
dev origins when VOCALIZE_HOST is 127.0.0.1/localhost, else
- the single production origin https://vocalize.dgpisces.com (D-10).
+ empty (operator MUST set this env var in non-localhost mode — see D-10).
VOCALIZE_WS_BASE_URL — REQUIRED when VOCALIZE_HOST is not localhost.
The public WS prefix echoed back from POST /api/sessions.
Raises RuntimeError at startup when absent in non-localhost mode
(closes Host-header spoofing vector D-11 — see CONCERNS.md).
In localhost-dev mode the WS URL is derived from the request base_url.
- VOCALIZE_INVITE_TOKEN — shared invite secret for POST /api/sessions;
- gate disabled in localhost-dev mode when unset (D-08).
GPU_HOST / SENSEVOICE_WS_PORT / COSYVOICE_WS_PORT — GPU service targets.
"""
app = FastAPI(title="VocalizeAI", version="0.1.0")
@@ -99,7 +97,7 @@ def create_app() -> FastAPI:
allow_origins=cors_origins,
allow_credentials=False,
allow_methods=["GET", "POST", "DELETE"], # D-10: explicit list; no wildcards
- allow_headers=["Content-Type", "X-Invite-Token"], # explicit; covers auth header
+ allow_headers=["Content-Type"], # explicit; no wildcards
)
registry = SessionRegistry()
@@ -126,7 +124,7 @@ async def _refresh_runtime_gauges_on_metrics_scrape(
raise RuntimeError(
"VOCALIZE_WS_BASE_URL is required when VOCALIZE_HOST is not localhost "
"(closes Host-header spoofing vector — see CONCERNS.md). "
- "Example: wss://vocalize-api.dgpisces.com"
+ "Example: wss://api.example.com"
)
register_session_routes(app, registry=registry)
diff --git a/src/vocalize/server/sessions.py b/src/vocalize/server/sessions.py
index 28f6704..848386c 100644
--- a/src/vocalize/server/sessions.py
+++ b/src/vocalize/server/sessions.py
@@ -7,10 +7,9 @@
import logging
import os
-import secrets
from typing import Literal
-from fastapi import Depends, FastAPI, Header, HTTPException, Request, status
+from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, Field
from vocalize.server.review import register_review_routes
@@ -52,45 +51,6 @@ class SetTaskResponse(BaseModel):
ok: bool = True
-def _check_invite_token(
- x_invite_token: str | None = Header(default=None, alias="X-Invite-Token"),
-) -> None:
- """Verify the shared invite secret on session creation (D-08).
-
- Gate behaviour:
- - If VOCALIZE_INVITE_TOKEN is not configured (localhost-dev mode), the
- check is skipped so local development keeps working without any env setup.
- - In production (non-localhost host), the token is required and must
- match via constant-time comparison to avoid timing oracles (T-04c-02).
-
- # TODO(v1.x AUTH-01): no rotation; this is a long-lived shared secret.
- # Per-user auth replaces this gate in v1.x.
- """
- from vocalize.config import get_config
-
- expected = get_config().invite_token
- if expected is None:
- return # localhost-dev mode: gate disabled
- try:
- match = secrets.compare_digest(x_invite_token or "", expected)
- except (TypeError, ValueError):
- # secrets.compare_digest raises TypeError when either string contains
- # non-ASCII characters. This is operator misconfiguration (e.g. a
- # Unicode passphrase). Log once and return 401 — do not propagate as
- # a 500 which would increment vocalize_error_log_total toward the
- # D-05 budget (T-04c-02).
- log.warning(
- "VOCALIZE_INVITE_TOKEN contains non-ASCII characters; "
- "compare_digest raised TypeError — returning 401"
- )
- match = False
- if not match:
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="invalid or missing X-Invite-Token",
- )
-
-
def register_session_routes(
app: FastAPI,
*,
@@ -108,7 +68,6 @@ def register_session_routes(
async def create_session(
request: Request,
payload: CreateSessionRequest | None = None,
- _gate: None = Depends(_check_invite_token),
) -> CreateSessionResponse:
registry.sweep_stale(max_age_s=1800)
payload = payload or CreateSessionRequest()
diff --git a/tests/test_app_startup_guards.py b/tests/test_app_startup_guards.py
index 7d377b0..9a7cfe6 100644
--- a/tests/test_app_startup_guards.py
+++ b/tests/test_app_startup_guards.py
@@ -29,7 +29,6 @@ def _make_client(monkeypatch, *, host: str, ws_base_url: str | None = None) -> T
monkeypatch.setenv("VOCALIZE_WS_BASE_URL", ws_base_url)
else:
monkeypatch.delenv("VOCALIZE_WS_BASE_URL", raising=False)
- monkeypatch.delenv("VOCALIZE_INVITE_TOKEN", raising=False)
monkeypatch.delenv("VOCALIZE_CORS_ORIGINS", raising=False)
reset_config()
return TestClient(create_app())
@@ -56,7 +55,6 @@ def test_localhost_without_ws_base_url_succeeds(monkeypatch):
def test_non_localhost_without_ws_base_url_raises(monkeypatch):
monkeypatch.setenv("VOCALIZE_HOST", "0.0.0.0")
monkeypatch.delenv("VOCALIZE_WS_BASE_URL", raising=False)
- monkeypatch.delenv("VOCALIZE_INVITE_TOKEN", raising=False)
monkeypatch.delenv("VOCALIZE_CORS_ORIGINS", raising=False)
reset_config()
with pytest.raises(RuntimeError, match="VOCALIZE_WS_BASE_URL is required"):
@@ -71,7 +69,7 @@ def test_non_localhost_with_ws_base_url_succeeds(monkeypatch):
client = _make_client(
monkeypatch,
host="0.0.0.0",
- ws_base_url="wss://vocalize-api.dgpisces.com",
+ ws_base_url="wss://api.example.com",
)
# App started without RuntimeError; 404 here just means session not found.
resp = client.get("/api/sessions/nonexistent-id")
@@ -89,7 +87,7 @@ def test_cors_localhost_origin_allowed(monkeypatch):
headers={
"Origin": "http://localhost:3000",
"Access-Control-Request-Method": "POST",
- "Access-Control-Request-Headers": "X-Invite-Token",
+ "Access-Control-Request-Headers": "Content-Type",
},
)
assert resp.status_code == 200
@@ -124,7 +122,7 @@ def test_cors_preflight_returns_explicit_methods_and_headers(monkeypatch):
headers={
"Origin": "http://localhost:3000",
"Access-Control-Request-Method": "POST",
- "Access-Control-Request-Headers": "Content-Type, X-Invite-Token",
+ "Access-Control-Request-Headers": "Content-Type",
},
)
assert resp.status_code == 200
@@ -135,8 +133,8 @@ def test_cors_preflight_returns_explicit_methods_and_headers(monkeypatch):
assert "POST" in allow_methods
assert "DELETE" in allow_methods
assert "*" not in allow_methods
- # Headers must include X-Invite-Token
- assert "X-Invite-Token" in allow_headers
+ # Headers must include Content-Type
+ assert "Content-Type" in allow_headers
# ---------------------------------------------------------------------------
@@ -147,7 +145,6 @@ def test_cors_wildcard_origins_raises_at_startup(monkeypatch):
monkeypatch.setenv("VOCALIZE_HOST", "127.0.0.1")
monkeypatch.setenv("VOCALIZE_CORS_ORIGINS", "*")
monkeypatch.delenv("VOCALIZE_WS_BASE_URL", raising=False)
- monkeypatch.delenv("VOCALIZE_INVITE_TOKEN", raising=False)
reset_config()
with pytest.raises(RuntimeError, match="VOCALIZE_CORS_ORIGINS must not contain"):
create_app()
@@ -159,9 +156,8 @@ def test_cors_wildcard_origins_raises_at_startup(monkeypatch):
def test_cors_wildcard_mixed_with_real_origins_raises(monkeypatch):
monkeypatch.setenv("VOCALIZE_HOST", "0.0.0.0")
- monkeypatch.setenv("VOCALIZE_WS_BASE_URL", "wss://vocalize-api.dgpisces.com")
+ monkeypatch.setenv("VOCALIZE_WS_BASE_URL", "wss://api.example.com")
monkeypatch.setenv("VOCALIZE_CORS_ORIGINS", "https://example.com,*")
- monkeypatch.delenv("VOCALIZE_INVITE_TOKEN", raising=False)
reset_config()
with pytest.raises(RuntimeError, match="VOCALIZE_CORS_ORIGINS must not contain"):
create_app()
diff --git a/tests/test_invite_token_gate.py b/tests/test_invite_token_gate.py
deleted file mode 100644
index 9682381..0000000
--- a/tests/test_invite_token_gate.py
+++ /dev/null
@@ -1,160 +0,0 @@
-"""Tests for the X-Invite-Token gate on POST /api/sessions (D-08).
-
-Covers:
- - Localhost-dev mode (no token configured): gate disabled, POST succeeds.
- - Production mode (token configured): gate enforces header presence + value.
- - SetTaskRequest.task length bound (max_length=2000).
- - Source assertion: _check_invite_token uses secrets.compare_digest.
-"""
-from __future__ import annotations
-
-import inspect
-
-import pytest
-from fastapi.testclient import TestClient
-
-from vocalize.config import reset_config
-from vocalize.server import create_app
-
-
-def _client(monkeypatch) -> TestClient:
- """Build a TestClient for the full production app.
-
- VOCALIZE_HOST is forced to 127.0.0.1 so create_app() does not raise the
- startup RuntimeError for missing VOCALIZE_WS_BASE_URL (Task 2).
- VOCALIZE_CORS_ORIGINS and VOCALIZE_WS_BASE_URL are cleared to prevent a
- CI environment with VOCALIZE_CORS_ORIGINS='*' from raising RuntimeError
- inside create_app() and masking the invite-token test failures.
- """
- monkeypatch.setenv("VOCALIZE_HOST", "127.0.0.1")
- monkeypatch.delenv("VOCALIZE_CORS_ORIGINS", raising=False)
- monkeypatch.delenv("VOCALIZE_WS_BASE_URL", raising=False)
- reset_config()
- return TestClient(create_app())
-
-
-# ---------------------------------------------------------------------------
-# Test 1: No token configured → localhost-dev gate disabled, POST succeeds
-# ---------------------------------------------------------------------------
-
-def test_no_token_configured_allows_session_creation(monkeypatch):
- monkeypatch.delenv("VOCALIZE_INVITE_TOKEN", raising=False)
- client = _client(monkeypatch)
- resp = client.post("/api/sessions")
- assert resp.status_code == 200
- assert "session_id" in resp.json()
-
-
-# ---------------------------------------------------------------------------
-# Test 2: Token configured, header missing → 401
-# ---------------------------------------------------------------------------
-
-def test_missing_header_returns_401_when_token_configured(monkeypatch):
- monkeypatch.setenv("VOCALIZE_INVITE_TOKEN", "abc123")
- client = _client(monkeypatch)
- resp = client.post("/api/sessions")
- assert resp.status_code == 401
- assert resp.json()["detail"] == "invalid or missing X-Invite-Token"
-
-
-# ---------------------------------------------------------------------------
-# Test 3: Token configured, wrong header value → 401
-# ---------------------------------------------------------------------------
-
-def test_wrong_header_value_returns_401(monkeypatch):
- monkeypatch.setenv("VOCALIZE_INVITE_TOKEN", "abc123")
- client = _client(monkeypatch)
- resp = client.post("/api/sessions", headers={"X-Invite-Token": "wrong-value"})
- assert resp.status_code == 401
- assert resp.json()["detail"] == "invalid or missing X-Invite-Token"
-
-
-# ---------------------------------------------------------------------------
-# Test 4: Token configured, correct header value → 200 + session_id
-# ---------------------------------------------------------------------------
-
-def test_correct_header_value_allows_session_creation(monkeypatch):
- monkeypatch.setenv("VOCALIZE_INVITE_TOKEN", "abc123")
- client = _client(monkeypatch)
- resp = client.post("/api/sessions", headers={"X-Invite-Token": "abc123"})
- assert resp.status_code == 200
- data = resp.json()
- assert "session_id" in data
- assert data["session_id"]
-
-
-# ---------------------------------------------------------------------------
-# Test 5: SetTaskRequest.task > 2000 chars → 422
-# ---------------------------------------------------------------------------
-
-def test_task_length_over_2000_returns_422(monkeypatch):
- monkeypatch.delenv("VOCALIZE_INVITE_TOKEN", raising=False)
- client = _client(monkeypatch)
- # Create a session first
- sess_resp = client.post("/api/sessions")
- assert sess_resp.status_code == 200
- session_id = sess_resp.json()["session_id"]
-
- oversized_task = "x" * 2001
- resp = client.post(
- f"/api/sessions/{session_id}/task",
- json={"task": oversized_task},
- )
- assert resp.status_code == 422
-
-
-# ---------------------------------------------------------------------------
-# Test 6: SetTaskRequest.task == 2000 chars → 200
-# ---------------------------------------------------------------------------
-
-def test_task_length_exactly_2000_is_accepted(monkeypatch):
- monkeypatch.delenv("VOCALIZE_INVITE_TOKEN", raising=False)
- client = _client(monkeypatch)
- sess_resp = client.post("/api/sessions")
- assert sess_resp.status_code == 200
- session_id = sess_resp.json()["session_id"]
-
- max_task = "x" * 2000
- resp = client.post(
- f"/api/sessions/{session_id}/task",
- json={"task": max_task},
- )
- assert resp.status_code == 200
-
-
-# ---------------------------------------------------------------------------
-# Test 7: Source assertion — _check_invite_token uses secrets.compare_digest
-# ---------------------------------------------------------------------------
-
-def test_check_invite_token_uses_timing_safe_comparison():
- from vocalize.server.sessions import _check_invite_token # noqa: PLC0415
-
- source = inspect.getsource(_check_invite_token)
- assert "secrets.compare_digest" in source, (
- "_check_invite_token must use secrets.compare_digest for timing-safe "
- "comparison (T-04c-02)"
- )
-
-
-# ---------------------------------------------------------------------------
-# Test 8: Non-ASCII invite token → 401 (not 500) — regression for WR-04
-# ---------------------------------------------------------------------------
-
-def test_non_ascii_invite_token_returns_401_not_500(monkeypatch):
- """secrets.compare_digest raises TypeError when VOCALIZE_INVITE_TOKEN contains
- non-ASCII characters (e.g. a Unicode passphrase set by the operator).
-
- HTTP headers are ASCII-only, so the client always sends an ASCII (or empty)
- token value; the non-ASCII value lives in the expected side only.
- The gate must catch TypeError and return 401, not propagate a 500 that would
- increment vocalize_error_log_total toward the D-05 error budget (T-04c-02).
- """
- monkeypatch.setenv("VOCALIZE_INVITE_TOKEN", "パスワード123")
- client = _client(monkeypatch)
- # Send an ASCII token value; the server side holds the non-ASCII expected value.
- # compare_digest raises TypeError when either argument is non-ASCII.
- resp = client.post("/api/sessions", headers={"X-Invite-Token": "ascii-token"})
- assert resp.status_code == 401, (
- f"Expected 401 for non-ASCII configured token, got {resp.status_code}"
- )
- assert resp.json()["detail"] == "invalid or missing X-Invite-Token"