diff --git a/AGENTS.md b/AGENTS.md index 4e9ddce..15349d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ - Core summary: within higher-priority instructions, the latest explicit user decision defines the task; take only requested actions and proven necessary consequences, and do not treat review/research/implementation as authority for implementation/deployment/cleanup beyond its task type. Broad words such as improve or optimize add no unnamed scope. Prefer standards and one owning invariant over example-specific mechanisms, while preserving unrelated work and necessary callers/tests/security. Reconstruct each deliverable from the accepted current result so it stands alone without rejected alternatives, process narration, or modification traces; inspect only the pending changes and report the result with completion evidence. The full skill governs when this summary is incomplete. - Highest priority for repository changes: before implementation, cleanup, or deployment, settle the meaning against the whole product model and evidence, challenge contradictions or needless complexity, and write the accepted decision to its owning truth documents. Never turn a partial interpretation into code or a branch. - Until the owner explicitly declares a public release, maintain only the current internal product contract. Do not preserve backward or forward compatibility, legacy wire/config aliases, dual parsers or writers, translators, deprecated APIs, migration shims, or tests whose only consumer is an older release. Upgrade the private deployment atomically, reject stale clients before authority, delete replaced surfaces, and use Git history for rollback. Reassess compatibility only when public release requirements are explicitly opened. -- Before dependent work starts, update every affected owning requirement, design, ADR or research conclusion, plus `docs/project-memory.md` and `docs/status.md` when their snapshots change. Checkpoint the consistent truth in Git; chat is not durable truth. +- Before dependent work starts, update the single actual owner among requirement, design, ADR or research, and update `docs/project-memory.md` or `docs/status.md` only when its snapshot materially changes. Do not create parallel owners or mirror source details across documents. Checkpoint the consistent truth in Git; chat is not durable truth. - If semantics remain disputed, record the hold in `docs/todo.md` and freeze only dependent work. Do not encode a guess as accepted truth. - Queue new observations in their owner and continue the active milestone unless the user requests immediate investigation or the evidence reveals a P0 blocker. @@ -39,7 +39,7 @@ - Start substantial work by reading `docs/project-memory.md`, `docs/status.md`, `docs/todo.md`, the relevant requirement/design, ADR, and research documents, then inspect the current branch and Git state. - Current requirements and design live in `docs/需求理解.md` and `docs/方案设计.md`; decisions live in `docs/adr/`; evidence lives in `docs/research/`. - `docs/project-memory.md` is the durable product snapshot, `docs/status.md` is the current execution/deployment index, and `docs/todo.md` is the only current work ledger. Git history owns the completed timeline. -- Give each fact one owner, replace stale text in place, keep status as an index rather than an archive, and update `docs/README.md` when documents move or change status. +- Give each durable fact one owner and replace stale text in place. Long-lived documents record product contracts, non-obvious invariants, algorithms/models, key tradeoffs, external evidence, and current execution boundaries; ordinary UI, self-evident implementation, routine validation process, and completed history stay in code, tests, pull requests, and Git. Keep status as an index rather than an archive, and update `docs/README.md` when documents move or change status. ## 6. Engineering Defaults diff --git a/docs/adr/0002-memory-resident-protected-rooms.md b/docs/adr/0002-memory-resident-protected-rooms.md index fa1dc03..27ade71 100644 --- a/docs/adr/0002-memory-resident-protected-rooms.md +++ b/docs/adr/0002-memory-resident-protected-rooms.md @@ -1,8 +1,7 @@ # ADR-0002: Memory-Resident Rooms And Scoped Viewer Access -- Status: Accepted; the existing room model is deployed in exact release - `c4962f5`, while the room-lived short grant and explicit missing-room result - are pending implementation +- Status: Accepted; current source implements the strict v12 room model, while + production remains exact release `c4962f5` on the older strict v11 contract - Date: 2026-08-23 ## Context @@ -95,12 +94,12 @@ Code entry is a separate two-state policy: - `open` is the default: after site access, the four-digit code admits a Viewer without a room password; -- `password`: after site access, the code also requires the room password. +- `private` disables passwordless code entry. Without a room password the room + is invitation-only; with one configured, matching code-and-password entry is + also admitted. -The Host UI presents these as a globe for public code entry and a lock for -password code entry. The password input appears only for the password state. -There is no code-entry-disabled state because it is easily mistaken for -disabling the whole room and is not required by either accepted entry path. +There is no third code-entry-disabled state; invitation access remains owned by +the independent Viewer grant. The Host may rotate or revoke the grant without changing code-entry policy, and may change code-entry policy without changing a healthy media route. Strong @@ -157,8 +156,8 @@ the generic `SERVER_ERROR`. None of those paths is folded into - Default creation atomically produces `open` code entry with no room password and an independent 22-character token-bearing invitation. The grant remains valid for exactly the current room incarnation and has no independent expiry. - `open` and `password` code entry are covered independently from grant - rotate/revoke. + `open` and `private` code entry, including private rooms with and without a + password, are covered independently from grant rotate/revoke. - Same-browser recreation reapplies the Host profile; another browser or cleared storage does not. No fingerprint or server user record participates. - Raw site passwords, Host tokens, Viewer grants, and room passwords remain out @@ -172,10 +171,9 @@ the generic `SERVER_ERROR`. None of those paths is folded into ## Implementation Status -Canonical `main` contains exact source implementation -`064db16c1b6d25fd24b7057b7e2fca172002e3c4` on the strict `screener-v12` -wire. It implements the 22-character room-incarnation grant, exact digest -validation, rotate/revoke, browser-storage privacy, and the `ROOM_NOT_FOUND` +Current source uses the strict `screener-v12` wire and implements the +22-character room-incarnation grant, exact digest validation, rotate/revoke, +browser-storage privacy, `open | private` code entry, and the `ROOM_NOT_FOUND` split without a v11 parser. Production remains exact deployed revision `c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`, on strict `screener-v11`; the v12 source has not been deployed. diff --git a/docs/deployment.md b/docs/deployment.md index b26bc29..00eacaf 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -367,11 +367,6 @@ Room allocation and lifetime use one deliberately small model: and routes; and - process restart has the same fail-closed effect. -Code-only entry is independent of the invitation: after site access, `open` -accepts the four-digit code without a room password; `password` also requires -the room password; and `disabled` rejects code-only entry while a valid grant -still enters. - The server keeps only process-memory digests/verifiers for Host tokens, Viewer grants, and optional room passwords. There is no room database, schema, migration, writable room directory, or backup/restore step. The same browser may @@ -434,8 +429,7 @@ Keep the proxy's access-log retention bounded and access controlled. Requests to `/r/{code}` put the room code in the path, so access logs can contain room codes as well as network metadata. They must not be treated as public artifacts. For a private room, the grant remains in the fragment and is not part of that -request target. Every code-only attempt requires prior site access; a disabled -policy then rejects that path, while a valid grant remains the direct Viewer path. +request target. For a process-level liveness probe, send `GET /healthz`. A running process returns HTTP 200 with `{"status":"ok"}` and `Cache-Control: no-store`; other diff --git a/docs/maintenance.md b/docs/maintenance.md index 7e97f1a..cee33e6 100644 --- a/docs/maintenance.md +++ b/docs/maintenance.md @@ -37,7 +37,7 @@ ## 机制减负审查 -机制减负遵循 [`stop-that-shit`](../.agents/skills/stop-that-shit/SKILL.md) 的 scope 和 smallest-correct-result 规则。本仓库的审查先只读清点机制服务的用户行为,量化移除后的真实退让,并与代码、配置、迁移、测试、运行时状态、部署和故障面的整体成本比较。若 owner 接受有界退让,先更新产品合同再删除失去必要性的完整表面;否则保留合同并寻找更小实现。审查不以行数为目标,也不授权无边界重构。 +机制减负遵循 [`stop-that-shit`](../.agents/skills/stop-that-shit/SKILL.md) 的 scope 和 smallest-correct-result 规则。本仓库的审查先只读清点机制服务的用户行为,量化移除后的真实退让,并与代码、配置、迁移、测试、运行时状态、部署和故障面的整体成本比较。文档也是需要同步和审查的机制:长期真相只保留产品合同、非显然不变量、算法/模型、关键取舍和外部证据,普通 UI 与实现细节由代码、测试和 PR 持有。若 owner 接受有界退让,先更新产品合同再删除失去必要性的完整表面;否则保留合同并寻找更小实现。审查不以行数为目标,也不授权无边界重构。 内存房间与应用层 TURN 分别是 [ADR-0002](./adr/0002-memory-resident-protected-rooms.md) 和 [ADR-0005](./adr/0005-automatic-hybrid-media-routing.md) 已接受的机制减负实例;TURN 的完整成本证据由 [ICE/TURN 研究](./research/built-in-peer-ice-turn.md)维护。具体取舍和当前边界不在本文件复制,也不推广为对 SQLite 或 TURN 的通用结论。 diff --git a/docs/project-memory.md b/docs/project-memory.md index 20a76d8..c2fcecc 100644 --- a/docs/project-memory.md +++ b/docs/project-memory.md @@ -1,6 +1,6 @@ # Project Memory -Last updated: 2026-08-24 +Last updated: 2026-08-25 ## Current Product Truth @@ -8,7 +8,7 @@ Last updated: 2026-08-24 - A room supports one Host and up to `20` authenticated Viewers. Admission is independent of endpoint fanout and SFU capacity; the event-driven route controller must remain efficient at that bound without all-pairs probing. - Authenticated Hosts and shipped clients are trusted media participants in these private rooms; route authority, generation fencing, and server admission remain fail-closed. - Rooms use one bounded, process-memory model: a random free code from `1000..9999`, default 24-hour configurable dormant lease, and no database or cross-restart recovery. Active sharing never expires; stop/disconnect starts the lease, the exact Host token may resume before expiry, Viewer activity does not renew it, and expiry releases the code. Process restart invalidates every room, token, invitation, password verifier, and participant. -- Viewer grants and code entry are orthogonal. Each room has one random 128-bit/22-character grant digest bound to that exact in-memory incarnation; it has no independent expiry and ends on reclamation, restart, rotate, or revoke. Every new room creates that invitation and defaults code entry to public `open` with no password; the only other code-entry state is `password`. An unallocated/reclaimed/expired code returns `ROOM_NOT_FOUND` without a password prompt, while other existing-room denials remain `ROOM_ACCESS_DENIED`. The Host browser retains its creation preferences and raw ownership token; the server keeps only current-process digests/verifiers and uses no fingerprint. Recreation reapplies preferences but creates a new code and credentials. +- Viewer grants and code entry are orthogonal. Each room has one random 128-bit/22-character grant digest bound to that exact in-memory incarnation; it has no independent expiry and ends on reclamation, restart, rotate, or revoke. Every new room creates that invitation and defaults code entry to public `open` with no password; the only other code-entry state is `private`. A private room with no password is invitation-only, while setting a password also permits matching code-only entry. An unallocated/reclaimed/expired code returns `ROOM_NOT_FOUND` without a password prompt, while other existing-room denials remain `ROOM_ACCESS_DENIED`. The Host browser retains its creation preferences and raw ownership token; exact-token invitation/policy/password management remains available while dormant without starting media or renewing the lease. The server keeps only current-process digests/verifiers and uses no fingerprint. Recreation reapplies preferences but creates a new code and credentials. - Web is the current delivery target. The route release covers Browser Host, Viewer, and relay endpoints; packaged/native senders, capture helpers, shared-encode executables, and their acceptance runs are lowest-priority later work outside this milestone. - Keep routing automatic and media distributed. Direct/peer UDP is preferred; the dedicated LiveKit SFU/UDP path is the only application fallback and is not the default topology. Screener configures no TURN, ICE/TCP, media TCP, or TLS-relayed media path. - Every non-server endpoint uses one server-authoritative steady outbound media-copy capacity: default `2`, statically configurable as `1`, `2`, or `3`. A peer child or the Host publication consumes one slot; upstream receive is free, and SFU subscriber egress is accounted at the server. Browser role/UA/visibility does not create a separate tier, and clients cannot raise the deployment value. @@ -28,13 +28,13 @@ Last updated: 2026-08-24 ## Current Source And Production -- Production is exact revision `c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`, on strict `screener-v11`. Canonical `main` contains source implementation `064db16c1b6d25fd24b7057b7e2fca172002e3c4` on strict `screener-v12`. +- Production is exact revision `c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`, on strict `screener-v11`. Current source uses strict `screener-v12`. - Current source and production admit up to `20` Viewers and implement the uniform `1/2/3` endpoint cap, exact prepare candidate, first-decoded-frame commit, strict rollback revision, decoded-progress stall, a two-stage direct/SFU operation deadline, and one event-driven room controller. A short-lived exact-generation decoded-frame observer proves only the pending candidate inside the existing total deadline; the periodic stats sampler remains responsible for active-path diagnostics and stalls. Ordinary peer ICE is STUN-only and the sole application fallback is SFU/UDP; stale TURN environment keys fail startup. SFU admission uses exact publication and Viewer-subscription handles with explicit no-default ingress/egress capacities and a listener-fenced dedicated LiveKit owner. Browser SFU publisher/subscriber connects use fresh empty ICE-server options; the owner physically verified deployed SFU fallback media after release `c4962f5`, closing the prior core-route hold. - Both use random four-digit memory rooms, dormant leases, exact-Host resume, restart loss, local preference replay, orthogonal grants/code entry, and no SQLite runtime. V12 source uses a 22-character room-incarnation grant, `ROOM_NOT_FOUND`, no diagnostic-file export, and a compact UI showing the complete shorter URL. Production v11 retains the prior grant and UI until an authorized atomic release. - Exact production artifacts, services, ports, and any change-scoped infrastructure recovery boundary are owned by [deployment](./deployment.md) and indexed by [status](./status.md). - Current source requires v12 and production requires v11; both fix Browser direct, browser-relay, and SFU media to VP8, leave video `contentHint` unset, and contain no codec UI, quality state, wire field, fallback media codec, or share-lifetime codec branch. Both use `shareGeneration`-fenced Pause/Resume and implement live 64/128/256 kbps audio-ceiling mutation, typed first-frame Viewer presentation, exactly three recommended profiles with `1080p30` as default, and advanced-only `854x480`. Current source additionally binds Host and SFU publisher audio metrics to the exact current sender track. - Browser SFU subscribers reconcile eligible Host screen publications across connect/activate event ordering, participant/track arrival, and reconnect. Production LiveKit logs only warn/error, and the application service has a bounded same-host LiveKit readiness preflight. -- Browser v12 implementation `064db16c1b6d25fd24b7057b7e2fca172002e3c4` passed TypeScript, 604 Web tests, both builds, the 255-test SFU gate, Chrome access/privacy and 20-Viewer gates, repository hygiene, review, and responsive visual checks. It is not deployed; the earlier SFU fix retains owner physical proof. +- Current Browser v12 source passed TypeScript, 609 Web tests in 44 files, both builds, repository hygiene, and focused review. Its exact predecessor owns the separately bounded SFU, Chrome access/privacy, 20-Viewer, and responsive visual evidence. V12 is not deployed; the earlier SFU fix retains owner physical proof. - Chrome loopback, cap2/cap3, short resource, recovery, MBB, and A/V fixtures are bounded evidence only. They do not establish quality, resource limits, or product policy. ## Current Priority diff --git a/docs/research/low-server-media-routes.md b/docs/research/low-server-media-routes.md index 0fd4a7c..83a6478 100644 --- a/docs/research/low-server-media-routes.md +++ b/docs/research/low-server-media-routes.md @@ -404,25 +404,6 @@ The [W3C File API](https://www.w3.org/TR/FileAPI/) (accessed 2026-08-22) defines Blob URLs for locally generated downloads and requires explicit revocation to release their backing store. -### Pre-Share Self-Check Boundary - -A temporary, unpaired `RTCPeerConnection` can create a data channel, set its -local offer, and inspect only `RTCIceCandidate.type`. Gathering a `srflx` -candidate proves that the browser received a STUN response for this generation; -it proves neither a usable peer candidate pair nor bandwidth, latency, NAT type, -or media quality. A same-origin WebSocket open followed by immediate close proves -only the signaling handshake. Neither probe creates a Screener room or media -edge, and both discard their temporary resources. - -Pinned LiveKit `Room.prepareConnection(url)` without a token performs an HTTP -`HEAD`; it does not establish ICE/UDP. A real LiveKit transport requires a token -and participant connection, which would create the room/SFU state this pre-share -check deliberately avoids. The UI therefore reports configured SFU as unknown -until an actual controller-selected route proves current-generation media. The -public, `no-store` self-check config returns only the existing STUN-only -`iceConfig` and an SFU-configured boolean. It returns no SFU URL, token, -credential, address, or candidate. - ## Historical SFU/UDP And Selected-Edge TURN Slice Every ordinary bounded peer `RTCPeerConnection` used STUN-only ICE. The tested diff --git a/docs/research/peer-assisted-media.md b/docs/research/peer-assisted-media.md index c33623d..02aca9c 100644 --- a/docs/research/peer-assisted-media.md +++ b/docs/research/peer-assisted-media.md @@ -392,49 +392,20 @@ capacity. The standalone ADR-0004 spike relied on controlled join order; the ADR-0005 controller starts each Viewer at zero and waits for an authenticated capacity message. There is no mobile/iPad, UA, or visibility capacity branch. -## Selected-Pair Response Observation +## Selected-Pair Resolution -The W3C Stats specification makes `RTCTransportStats.selectedCandidatePairId` -the exact reference to the current `RTCIceCandidatePairStats`. A stats object's -`id` identifies the monitored object, while `responsesReceived` is the -cumulative number of connectivity-check responses received by that candidate -pair. Stats snapshots carry timestamps and applications derive interval values -from two snapshots. Unsupported fields are omitted, and a current pair is -deleted when the transport switches to a newly generated pair. These semantics -support a local observation; they do not define an application failure signal. -The existing compatibility fallback is retained only when that exact reference -is absent and exactly one same-transport succeeded nominated/selected pair is -available; an ambiguous or broken relationship remains unknown. +`RTCTransportStats.selectedCandidatePairId` identifies the current +`RTCIceCandidatePairStats` used for path, candidate, and round-trip metrics. When +that exact reference is absent, a fallback is valid only if exactly one +same-transport succeeded nominated or selected pair exists; an ambiguous or +broken relationship remains unknown. -RFC 7675 binds consent to one transport 5-tuple. An authenticated matching STUN -response refreshes consent, but the default consent-check interval is randomized -between four and six seconds and a response can match an earlier request. -Therefore zero `responsesReceived` growth over one two-second application sample -does not prove a silent partition. Conversely, growth proves only that a STUN -response was observed on that pair, not that media quality or future reachability -is healthy. Video-frame counters are independent, so a static shared frame must -not be classified as a transport failure. - -The bounded implementation uses one accumulator per `RTCPeerConnection` and the -selected pair's own stats timestamp. It reports the cumulative counter plus a -delta and elapsed milliseconds only across adjacent samples of the same pair. -The first sample, PC replacement, pair change or disappearance, missing/invalid -counter or timestamp, non-increasing timestamp, counter retreat, or a sampling -gap above five seconds reports the interval fields as unknown and establishes a -new baseline. The five-second ceiling is a named observation-window bound for -the current two-second sampler; it is not a consent timeout or a route policy. - -Acceptance for this slice is observation-only: - -- expanded local connection details show the opaque selected-pair identity, - cumulative `responsesReceived`, and the adjacent bounded delta/window; -- the click-only diagnostic allowlist may contain the cumulative counter, - interval delta, and interval duration, but not the pair ID, candidate - addresses/ports, raw stats, signaling, or credentials; -- a missing response counter or required timestamp remains unknown rather than - zero; and -- no value starts ICE restart, emits `route-failed`, changes topology/quality, - adds a wire or server ping, or uploads/persists telemetry. +`responsesReceived` is a cumulative connectivity-check response counter, not a +media-health signal. RFC 7675 consent checks normally occur less often than the +application stats sampler, so a zero interval does not prove a silent partition, +while growth does not prove media quality or future reachability. Screener does +not expose or route on this counter, and the accepted Browser contract does not +retain it or a derived sampling window. Sources, accessed 2026-08-22: @@ -550,8 +521,11 @@ socket generation are ignored, and the server revalidates the current authenticated session before replying. A hidden document, a visibility restore, or an obviously late timer callback clears pending evidence and starts a fresh five-second baseline instead of declaring failure. -This response-only watchdog is deployed in production; -real silent-partition timing remains a production/browser evidence boundary. +This response-only watchdog is deployed in production and remains limited to +authenticated, visible sessions with an active authoritative route. Hidden or +background lifecycle is rebaselined rather than treated as failure; the watchdog +does not provide page or media keepalive. Mobile background behavior remains a +separate physical-device evidence boundary. The response is socket-local, contains no room state, secret, candidate, or raw statistics, is not logged, and is limited by the server to at most one response diff --git a/docs/research/webrtc-p2p-screen-sharing.md b/docs/research/webrtc-p2p-screen-sharing.md index 62dc4b1..d9b4761 100644 --- a/docs/research/webrtc-p2p-screen-sharing.md +++ b/docs/research/webrtc-p2p-screen-sharing.md @@ -282,7 +282,7 @@ Electron 可以固定 Chromium 版本,枚举屏幕/窗口,改善选源、热 ### 最小访问模型 -接受的 access boundary 使用 production 必配的 `SITE_ACCESS_PASSWORD` 无状态 cookie 保护建房、Host role 和所有 code-only Viewer 入口;合法未过期的 exact-room fragment grant 可直接观看。没有 grant 时,服务端先要求 site access,再检查 `open | password` code-only policy;房间码只定位,cookie 也不替代房间密码。WebSocket upgrade 仍只做 Origin/容量检查并记录 cookie 状态,首条 `authenticate` 完成上述 role/room 授权。该边界不引入账号、JWT、服务端 session Map 或逐人 ACL。 +接受的 access boundary 使用 production 必配的 `SITE_ACCESS_PASSWORD` 无状态 cookie 保护建房、Host role 和所有 code-only Viewer 入口;合法未过期的 exact-room fragment grant 可直接观看。没有 grant 时,服务端先要求 site access,再检查 ADR-0002 持有的 code-only policy;房间码只定位,cookie 也不替代房间密码。WebSocket upgrade 仍只做 Origin/容量检查并记录 cookie 状态,首条 `authenticate` 完成上述 role/room 授权。该边界不引入账号、JWT、服务端 session Map 或逐人 ACL。 RFC 3986 的规范事实是 fragment 在 URI dereference 前由 user agent 分离;WHATWG WebSockets 进一步规定含 fragment 的 constructor URL 必须抛 `SyntaxError`。因此把 128-bit room grant 放在 `/r/{code}#v=...`,再由页面在首个 WSS application message 发送,可以使它不进入 HTTP 或 WebSocket request-target。RFC 6750 对 OAuth bearer query 的警告并不直接规定本产品,但它提供了适用的安全类比:URI query 高概率被日志记录,不应承载此 grant。W3C Referrer Policy 的算法会从 referrer URL 移除 fragment,production 的 `no-referrer` header 再禁止整个 header;这是传输边界,不是“不会泄漏”的保证。 diff --git a/docs/session-handoff.md b/docs/session-handoff.md index b1b73de..6d5a7c2 100644 --- a/docs/session-handoff.md +++ b/docs/session-handoff.md @@ -46,15 +46,9 @@ Current high-signal boundary: diagnostics and stalls. Broader external-network and SFU lifecycle evidence remains open. Do not run executable/native suites unless their later acceptance boundary specifically requires them. -- ADR-0002 owns the room model: one process-memory RoomStore, random free - four-digit codes, configurable 24-hour dormant leases, restart loss, local - Host preference replay, and open/password/disabled code entry. Canonical - `main` contains exact v12 source implementation - `064db16c1b6d25fd24b7057b7e2fca172002e3c4`: a 22-character room-lived - opaque grant, `ROOM_NOT_FOUND`, exact-current audio stats, no product - diagnostic download, and complete shorter invitation URLs. The Host-only - bounded snapshot remains for acceptance tooling. Production stays exact - `c4962f5` on v11 until an explicitly authorized atomic application release. +- ADR-0002 owns the room model. Use `docs/status.md` for the exact current-source + and production boundary; production stays exact `c4962f5` on v11 until an + explicitly authorized atomic application release. Neither source nor production uses SQLite; exact release operations and change-scoped infrastructure recovery are owned by deployment. - Browser display video must leave `contentHint` unset; audio keeps `music`. diff --git a/docs/status.md b/docs/status.md index 3fba949..5f1a22e 100644 --- a/docs/status.md +++ b/docs/status.md @@ -1,6 +1,6 @@ # Current Status -Last updated: 2026-08-24 +Last updated: 2026-08-25 This is the current execution index. Git history owns completed timelines; [verification status](./verification-status.md) owns evidence boundaries. @@ -17,23 +17,22 @@ This is the current execution index. Git history owns completed timelines; [veri ## Current Source -- Canonical root `main` is the integration truth; auxiliary branches and older worktrees do not supersede it. Current source contains exact implementation `064db16c1b6d25fd24b7057b7e2fca172002e3c4` on the single strict Browser `screener-v12` wire and rejects every other wire before room authority. Production remains the older exact v11 revision stated above. -- Current source implements one Host plus up to `20` Viewers, one steady outbound media-copy cap for every non-server endpoint (default `2`, static `1/2/3`), and the process-memory room model with random four-digit codes, a default 24-hour dormant lease, exact-Host resume, restart loss, local Host preference replay, `open | password | disabled` code entry, and no SQLite runtime. Its Viewer grant is one 22-character opaque value bound to the exact room incarnation; unallocated/reclaimed/expired code-only entry returns `ROOM_NOT_FOUND` without a password prompt. +- Canonical root `main` is the integration truth; auxiliary branches and older worktrees do not supersede it. Current source uses the single strict Browser `screener-v12` wire and rejects every other wire before room authority. Production remains the older exact v11 revision stated above. +- Current source implements one Host plus up to `20` Viewers, one steady outbound media-copy cap for every non-server endpoint (default `2`, static `1/2/3`), and the process-memory room model with random four-digit codes, a default 24-hour dormant lease, exact-Host resume, restart loss, local Host preference replay, exactly `open | private` code entry, and no SQLite runtime. Every room defaults to `open` without a room password and independently receives a Viewer grant: one 22-character opaque value bound to that room incarnation. Private rooms without a password are invitation-only; configuring a password additionally permits matching code-only entry. Unallocated/reclaimed/expired code-only entry returns `ROOM_NOT_FOUND` without a password prompt. - One event-driven controller owns the committed graph and at most one room-serial child operation. It uses one deterministic candidate list/cursor and one total direct-then-SFU deadline; exact admission and physical resources remain charged through drain, and the pending candidate commits only after an exact-generation decoded-frame proof. A short-lived 100 ms stats observer proves only that candidate inside the existing deadline, while the 2-second sampler remains responsible for active-path diagnostics and decoded-frame stalls. Healthy edges remain sticky and current-path quality does not authorize reparenting. - Ordinary peer ICE is STUN-only and the only application fallback is the dedicated LiveKit SFU over UDP. SFU generations use explicit no-default ingress/egress capacities, exact `reserved | committed | draining` accounting, delete-plus-absence release, one Host publication, and exact Viewer subscription handles. No Screener TURN, ICE/TCP, media TCP, or TLS-relayed media route exists. - Current Browser SFU publisher and subscriber connects supply fresh `autoSubscribe: false` plus `rtcConfig: { iceServers: [] }` options. Ordinary peers retain deployment STUN; LiveKit signaling candidates, server-side public-IP discovery, route deadlines, ports, and retry behavior are unchanged. - Current source leaves video `contentHint` unset and keeps audio `contentHint = "music"`. Browser direct and browser-relay offers contain VP8 as their only media codec, and SFU publication explicitly uses VP8 with no backup codec. Codec UI, quality state, wire fields, fallback media codecs, and share-lifetime codec branches are absent. Live 64/128/256 kbps audio-ceiling mutation remains unchanged; ordinary Host and SFU publisher metrics now select the exact current audio sender track and reset their baseline when that identity changes or disappears. -- Current source implements typed first-frame Viewer presentation and recovery, Chinese user-facing route/access failures, local connection details without a diagnostic-file export, authenticated Host-only on-demand route snapshots for automatic acceptance, explicit missing-room presentation, and responsive entry controls. The Host invitation surface shows and copies the complete shorter URL and retains independent rotate/revoke, room-code, policy, and password controls. +- Current source implements typed first-frame Viewer presentation and recovery, Chinese user-facing route/access failures, local connection details without a diagnostic-file export, authenticated Host-only on-demand route snapshots for automatic acceptance, explicit missing-room presentation, and responsive entry controls. Host invitation controls remain independent from two-state room-code entry. - Recommended quality remains exactly `1080p60`, `1080p30`, and `720p30`; current source defaults to `1080p30`. Advanced resolution adds `480p` as `854x480` without adding a fourth preset, and advanced FPS and bitrate remain independent. -- Exact v12 source implementation `064db16c1b6d25fd24b7057b7e2fca172002e3c4` passed repository hygiene, TypeScript, all 604 Web tests in 45 files, both production builds, the 255-test SFU gate, the Chrome access/privacy gate, a 20-Viewer Chrome loopback, focused independent review, and desktop/narrow/mobile invitation and missing-room visual checks. It is not deployed. Exact evidence boundaries are owned by [verification status](./verification-status.md). +- Current v12 source passed repository hygiene, TypeScript, all 609 Web tests in 44 files, both production builds, and focused independent review. The exact predecessor's SFU, Chrome access/privacy, 20-Viewer loopback, and responsive visual evidence remains separately bounded in [verification status](./verification-status.md). V12 is not deployed. ## Current Milestone -1. Implement the accepted two-state `open | password` room-code contract on strict v12: remove `disabled`, preserve default `open` plus the independently generated invitation, and present public/password entry with globe/lock controls and an on-demand password field. Current source and production still retain the older three-state implementation at this truth checkpoint. -2. After explicit owner instruction, release the exact v12 source atomically and verify stale-wire rejection, health, invitation/access behavior, and unchanged direct/SFU operation; no deployment has occurred at this checkpoint. -3. Physically validate the corrected live screen-audio statistics and 64/128/256 kbps ceilings with known audible content across direct, browser-relay, and SFU paths. -4. Physically verify fixed-VP8/no-video-hint direct, browser-relay, and SFU paths from actual codec, capture, outbound, and decoded stats. -5. Finish representative ICE/STUN/SFU acceptance including mobile networks. Browser port prediction, NAT classification, TCP probing, fake page keepalive, and quality-driven reparenting remain outside the accepted model. +1. Release the accepted strict v12 source atomically and verify stale-wire rejection, health, invitation/access behavior, and unchanged direct/SFU operation. Private rooms without a password remain invitation-only, and a configured password additionally permits matching code-only entry; no deployment has occurred at this checkpoint. +2. Physically validate the corrected live screen-audio statistics and 64/128/256 kbps ceilings with known audible content across direct, browser-relay, and SFU paths. +3. Physically verify fixed-VP8/no-video-hint direct, browser-relay, and SFU paths from actual codec, capture, outbound, and decoded stats. +4. Finish representative ICE/STUN/SFU acceptance including mobile networks. Browser port prediction, NAT classification, TCP probing, fake page keepalive, and quality-driven reparenting remain outside the accepted model. ## Active Boundaries diff --git a/docs/todo.md b/docs/todo.md index bfb1ca7..aca3ce8 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,16 +1,16 @@ # Current TODO Ledger -Last reviewed: 2026-08-24 +Last reviewed: 2026-08-25 Only items in **Now** are executable after their stated decision gate. A branch name, unchecked requirement, experiment, review suggestion, or deployed behavior is not a TODO by itself. ## Now -1. **Implement the accepted two-state room-code contract.** Strict v12 must accept only `open | password`, default every new room to `open` without a room password, and independently generate its Viewer-grant invitation. Remove the current `disabled` state from protocol, storage, server authorization, client preferences, UI, and tests. Host UI uses globe/lock controls and shows the password field only when password entry is selected; the invitation surface remains independent. -2. **Deploy Browser v12 only after explicit owner instruction.** Current source implementation `064db16c1b6d25fd24b7057b7e2fca172002e3c4` already contains the strict v12 grant/access/UI and exact-audio-stat changes; production remains exact `c4962f5` on v11. A release must switch server and Browser assets atomically, reject stale v11 before room authority, run bounded health/postflight checks, and verify the shorter complete invitation URL, rotate/revoke, code-only missing-room result, direct media, and SFU fallback. This is an application-only release unless its scope is explicitly expanded. -3. **Physically validate live screen-audio statistics.** The source fix binds ordinary Host and SFU publisher audio stats to the exact current sender track, merges SFU video/audio reports, and resets the baseline when audio identity changes or disappears. After deployment, reproduce the reported `1 kbps` state with known audible content and switch the 64/128/256 kbps ceilings across direct, browser-relay, and SFU paths while checking sender/receiver counters, route continuity, audible result, and A/V synchronization. A real muted/idle track may legitimately use about 1 kbps; do not multiply the value or display the configured ceiling as observed bitrate. -4. **Physically validate fixed Browser VP8 with no video hint.** Production exact deployed application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`, runs strict v11, VP8-only direct/browser-relay offers, VP8 SFU publication with no backup codec, unset video `contentHint`, and no codec UI/state/wire. Confirm actual codec, capture, outbound, and decoded stats on direct, browser-relay, and SFU paths; automated gates and the owner SFU connectivity proof do not close the full codec gate. -5. **Finish real-network route acceptance.** Measure standard ICE/STUN direct paths, peer relay, SFU ingress/subscription, exact first-frame commit and rollback, relay-ingress repair with subtree retention, disconnect/capacity drain, Pause/Resume, and bounded explicit failure across representative IPv4/IPv6 NATs and mobile networks. Include an all-UDP-blocked case to establish the current failure boundary. Do not add port prediction, NAT classification, TCP probes, guessed candidates, route scores, or quality-driven reparenting. +1. **Finish the accepted private room-entry contract, then deploy Browser v12 only after explicit owner instruction.** Strict v12 must use only `open | private`: private without a password is invitation-only, while a configured password also permits matching code-only entry. Production remains exact `c4962f5` on v11. A release must switch server and Browser assets atomically, reject stale v11 before room authority, run bounded health/postflight checks, and verify the shorter complete invitation URL, rotate/revoke, public/private code entry with and without a password, code-only missing-room result, direct media, and SFU fallback. This is an application-only release unless its scope is explicitly expanded. +2. **Physically validate live screen-audio statistics.** The source fix binds ordinary Host and SFU publisher audio stats to the exact current sender track, merges SFU video/audio reports, and resets the baseline when audio identity changes or disappears. After deployment, reproduce the reported `1 kbps` state with known audible content and switch the 64/128/256 kbps ceilings across direct, browser-relay, and SFU paths while checking sender/receiver counters, route continuity, audible result, and A/V synchronization. A real muted/idle track may legitimately use about 1 kbps; do not multiply the value or display the configured ceiling as observed bitrate. +3. **Diagnose and physically validate Browser VP8 performance with no video hint.** First reproduce the reported localhost `1080p60` shortfall with current source and separate source capture, encoded, sent, received, decoded, and rendered frame rates plus encode time, quality limitation, CPU/GPU load, and actual encoder implementation. Compare `contentHint` only as a controlled diagnostic and do not restore it by assumption. Then confirm actual VP8 codec, capture, outbound, and decoded stats on direct, browser-relay, and SFU paths; automated gates and the owner SFU connectivity proof do not close this performance/codec gate. +4. **Finish real-network route acceptance.** Measure standard ICE/STUN direct paths, peer relay, SFU ingress/subscription, exact first-frame commit and rollback, relay-ingress repair with subtree retention, disconnect/capacity drain, Pause/Resume, and bounded explicit failure across representative IPv4/IPv6 NATs and mobile networks. Include an all-UDP-blocked case to establish the current failure boundary. Do not add port prediction, NAT classification, TCP probes, guessed candidates, route scores, or quality-driven reparenting. +5. **Apply the accepted local-observation cleanup.** Remove the selected-pair `responsesReceived` cumulative and interval counters, their sampling window, and their tests; retain exact selected-pair resolution for current path metrics. Keep the signaling watchdog limited to authenticated, visible sessions with an active authoritative route. Hidden/background transitions rebaseline instead of proving failure, and the watchdog does not provide page or media keepalive. ## Accepted Later Roadmap @@ -23,15 +23,13 @@ These items are real product work, ordered after the current physical-media, rea 5. **Fully local one-click Host-server package.** Package Windows/macOS/Linux Host capture, the exact application server and local state without requiring source, Node or any external Screener/STUN/SFU/tunnel/relay service. Preserve usable direct P2P and use the completed NAT/network evidence to maximize reachability, while reporting public-origin, gateway, NAT and firewall limits truthfully; loopback/LAN success is not universal Internet success. 6. **Native Host capture backend.** At lowest priority, productize a proven Windows native capture path before expanding capture/audio backends to macOS and Linux. Its capture, hardware encode, audio, identity and lifecycle evidence becomes an input to later packages; a source checkout, helper executable, or evaluation ZIP is not a distributable product. 7. **Native shared encode, Windows first.** At lowest priority, prove one hardware encoder output can serve two independent Viewer transports and the Host-SFU publication while each connection retains its own RTP/RTCP, pacing, encryption, feedback, and upload accounting. Productize only after actual access-unit identity, Viewer2/FIFO, heterogeneous feedback, game-resource, browser/hardware, A/V, packaging, and licensing gates pass. Web Host/relay remains standard per-`RTCPeerConnection` encoding because no portable shared-encoder contract exists. -8. **Repository simplification audit.** After the current Browser, physical-media, route, and room checkpoints settle, inventory components, configuration, migrations, gates, timers, compatibility paths, and tests using the [maintenance review](./maintenance.md#机制减负审查). Begin read-only: identify the user behavior each mechanism preserves, quantify the impact of removing it, and compare that value with its whole-system cost. Any accepted feature tradeoff updates its owning contract before complete unused surfaces are removed. This later audit must not become a broad refactor or block current product work. +8. **Whole-product UI polish and bilingual-scope decision.** After current functional, physical-media, and real-network work stabilizes, review the product UI once as a whole: simplify and align copy, refine responsive hierarchy and visual consistency, add restrained motion, and measure rendering and bundle cost. Decide Chinese/English scope, default/fallback and switching persistence before introducing one shared text catalog; do not add duplicate screen-specific translations or let presentation redefine room and route semantics. +9. **Repository simplification audit.** After the current Browser, physical-media, route, and room checkpoints settle, inventory components, configuration, migrations, gates, timers, compatibility paths, tests, and truth-document duplication using the [maintenance review](./maintenance.md#机制减负审查). Begin read-only: identify the behavior or decision each surface preserves and compare that value with its whole-system cost. Any accepted feature tradeoff updates its single owning contract before complete unused surfaces are removed. This later audit must not become a broad refactor or block current product work. ## Product Decisions Needed | Deployed or retained item | Decision | | --- | --- | -| #169 connection self-check | Keep, revise, or remove the user-facing pre-share probe. Do not extend it automatically. | -| #181 selected-pair response counter | Keep as local observation or remove; it has no route authority. | -| #182 signaling watchdog | Confirm Host and mobile/background session semantics before calling it complete. | | Project license and closed-commercial boundary | Decide the repository and distribution license before any public release or package distribution. GPL/AGPL implementations remain research-only until then. | These decisions are not permission to continue old branches, and current deployment remains unchanged until a scoped decision is made. diff --git a/docs/verification-status.md b/docs/verification-status.md index 5023397..303e7c6 100644 --- a/docs/verification-status.md +++ b/docs/verification-status.md @@ -18,7 +18,7 @@ Capacity and routing entries below describe the exact tested source or release o ## Retained Functional Evidence -- Exact v12 source implementation `064db16c1b6d25fd24b7057b7e2fca172002e3c4` passed repository hygiene, TypeScript typecheck, all 604 Web tests in 45 files, client and server production builds, the 255-test SFU admission gate in 10 files, the explicit-Chrome access/privacy gate, and the Chrome 20-Viewer loopback gate. Independent reviews found no remaining blocker. Real Chrome checks at 1440, 600, and 390 CSS pixels showed the complete 53-character local invitation URL without overflow; the 390-pixel room-password control stayed collapsed until requested and its editor fit when opened. A missing well-formed room showed `房间不存在或已过期`, no password control, and no horizontal overflow. This validates the current source contract and responsive surface only; it is not deployment, physical screen-audio, public-network, or real-SFU evidence. +- Exact v12 predecessor implementation `064db16c1b6d25fd24b7057b7e2fca172002e3c4` passed repository hygiene, TypeScript typecheck, all 604 Web tests in 45 files, client and server production builds, the 255-test SFU admission gate in 10 files, the explicit-Chrome access/privacy gate, and the Chrome 20-Viewer loopback gate. Independent reviews found no remaining blocker. Real Chrome checks at 1440, 600, and 390 CSS pixels showed the complete 53-character local invitation URL without overflow; the 390-pixel room-password control stayed collapsed until requested and its editor fit when opened. A missing well-formed room showed `房间不存在或已过期`, no password control, and no horizontal overflow. This validates that exact source and responsive surface only; it is not deployment, physical screen-audio, public-network, or real-SFU evidence. - Exact v11 implementation `f5a295c52e0ac7d18e5a7949217861c7aa74e9c9`, integrated with the SFU isolation fix into deployed application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996` and canonical `main`, passed TypeScript typecheck, all 605 Web tests in 45 files, client and server production builds, the 254-test SFU gate in 10 files, and the access/privacy gate with explicit Chrome. This validates source structure, strict-wire behavior, fixed-VP8 path configuration, build output, and the covered Browser gate only; it does not provide full physical direct/browser-relay/SFU codec evidence. - Exact Browser SFU ICE-server-isolation implementation `ae09c760adec76fd26da611d4928486d105c6d3b` passed repository hygiene, TypeScript typecheck, all 605 Web tests in 45 files, client and server production builds, three independent code reviews, focused 95-test publisher/subscriber and ordinary-peer isolation coverage, deployment postflight, and owner physical SFU fallback media proof. This establishes the reported production connection fix without turning candidate type/count into an invariant; full route/codec/network acceptance remains open. - A fresh canonical `main@c4962f5` background gate passed repository hygiene, all 605 Web tests, both production builds, the 254-test SFU admission suite, and the Chrome 151 20-Viewer 720p30/30-second local route gate. All 20 Viewers advanced decoded frames, Host and relays stayed within endpoint cap `2`, no orphan SFU publication appeared, and all four current-child timing distributions contained 20 values. The browser gate uses deterministic canvas and does not start LiveKit, so it proves local direct/browser-relay control and media behavior only; it is not physical capture or SFU evidence. diff --git a/docs/方案设计.md b/docs/方案设计.md index 8ae49cf..a43519a 100644 --- a/docs/方案设计.md +++ b/docs/方案设计.md @@ -2,7 +2,7 @@ ## 2026-08-18 14:45 -状态:canonical `main` 包含精确 source implementation `064db16c1b6d25fd24b7057b7e2fca172002e3c4` 和严格 `screener-v12`;production 仍运行精确 revision `c4962f54443ad5f98bc65861195a3d9c74a48996`、release `c4962f5` 与严格 `screener-v11`,v12 尚未部署。源码已实现 22 字符 room-lived Viewer grant、`ROOM_NOT_FOUND`、产品诊断下载移除、完整短 URL 邀请 UI、收敛的房间密码控件与 exact-current audio stats。源码和生产均固定 Browser VP8,video 不设置 `contentHint`、audio 保留 `music`,且无 codec UI/state/wire 或分享生命周期分支;Pause/Resume 继续由当前 Host session 和 `shareGeneration` 约束。Owner 已在生产实测现有 SFU fallback 成功取得画面。 +状态:current source 使用严格 `screener-v12`;production 仍运行精确 revision `c4962f54443ad5f98bc65861195a3d9c74a48996`、release `c4962f5` 与严格 `screener-v11`,v12 尚未部署。源码已实现 22 字符 room-lived Viewer grant、`ROOM_NOT_FOUND`、产品诊断下载移除、完整短 URL 邀请 UI、收敛的房间密码控件与 exact-current audio stats。源码和生产均固定 Browser VP8,video 不设置 `contentHint`、audio 保留 `music`,且无 codec UI/state/wire 或分享生命周期分支;Pause/Resume 继续由当前 Host session 和 `shareGeneration` 约束。Owner 已在生产实测现有 SFU fallback 成功取得画面。 ## 目标与边界 @@ -72,13 +72,15 @@ tests/ RoomStore、ICE 配置、协议和 WebSocket 集成测试 `SITE_ACCESS_PASSWORD` 是简单的部署访问口令,不是 Viewer 密码或账号凭据。production 启动时必须配置;本地开发/测试可显式关闭。非空值必须包含 8 至 128 个可见 ASCII bytes(`0x21` 至 `0x7e`),并作为不复用 LiveKit、TLS 或其他凭据的独立 secret。`POST /api/site-access` 只在严格允许的 Origin 下接收 `Authorization: Bearer`,成功后签发 12 小时的无状态 site-access cookie:值只包含版本、到期时间和 HMAC-SHA256 签名,不创建账号、JWT 或服务端 session Map。cookie 使用 `HttpOnly`、`SameSite=Strict`、`Path=/`、有限 `Max-Age`;HTTPS 生产环境使用 `Secure` 和 `__Host-` 前缀。`POST /api/rooms` 只认该 cookie,不接受同一 Bearer 旁路。原子 deployment change 在 nginx 为该 admission endpoint 配置 per-source `limit_req`,初始边界为 `5r/m`、`burst=5 nodelay`;超限返回通用 429,允许到达 Node 的错误 secret 使用常量时间比较和统一 401。limiter 只使用 nginx shared memory,不新增 IP 持久化/日志字段;Node 不建立 IP key/LRU/session table,并保留现有全局/未鉴权连接上限。删除旧 `ACCESS_PASSWORD`、`/api/session` 与旧 cookie;旧环境变量出现时 fail-fast。 -WebSocket upgrade 无法预知后续 role,因此仍允许没有 site-access cookie 的合法 grant Viewer 完成握手;握手前严格校验 `/signal` 无 query、Origin allowlist、总连接数和未鉴权连接数,并把当次 cookie 校验结果保存到 socket state。五秒内的第一条 `authenticate` 完成授权:Host 必须同时拥有 upgrade 时有效的 site-access 状态和当前房间 `hostToken`;Viewer 必须拥有目标房间当前 digest 对应的 grant,或者先通过 site access 再按 `open | password` code-only policy 校验。site-access cookie 只允许进行 code-only 尝试,不能替代 grant/房间密码;Viewer grant 也不能用于 HTTP 建房、Host role、其他房间或其他操作。 +WebSocket upgrade 无法预知后续 role,因此仍允许没有 site-access cookie 的合法 grant Viewer 完成握手;握手前严格校验 `/signal` 无 query、Origin allowlist、总连接数和未鉴权连接数,并把当次 cookie 校验结果保存到 socket state。五秒内的第一条 `authenticate` 完成授权:Host 必须同时拥有 upgrade 时有效的 site-access 状态和当前房间 `hostToken`;Viewer 必须拥有目标房间当前 digest 对应的 grant,或者先通过 site access 再按 `open | private` code-only policy 校验。site-access cookie 只允许进行 code-only 尝试,不能替代 grant/房间密码;Viewer grant 也不能用于 HTTP 建房、Host role、其他房间或其他操作。 Viewer grant 与 code-only 入口是两个正交维度。每个房间用 `randomBytes(16).toString("base64url")` 生成 128-bit、22 字符 room-scoped opaque grant,服务端只在该 exact room incarnation 内保存完整 grant 的 SHA-256 摘要并用定长比较验证。grant 不编码版本、房间号或到期时间,也没有独立 expiry;它随房间存在,并在房间回收、进程重启、rotate 或 revoke 时失效。同号新房间生成新 digest,旧链接不能进入。Host 的主分享动作始终复制 token-bearing invitation;即时清退使用 rotate/revoke。 -code-only policy 使用单一 `open | password` 状态:`open` 是无房间密码的默认值;`password` 要求已通过 site access 的 Viewer 再输入该房间密码。两种状态都不改变有效 grant 的 exact-room Viewer authority,也不建立目录、账号、ACL 或额外媒体容量。Host 可独立改变 code-only policy、设置/替换/移除密码,以及 rotate/revoke grant。Host UI 只用地球与锁表达公开加入和密码加入;选择密码加入时才显示密码输入,不呈现会被误解为整个房间不可访问的关闭状态。 +code-only policy 使用单一 `open | private` 状态:`open` 是无房间密码的默认值;`private` 在未配置密码时拒绝全部 code-only 请求,只允许有效 grant,配置密码后才接受已通过 site access 且密码匹配的 Viewer。两种状态都不改变有效 grant 的 exact-room Viewer authority,也不建立目录、账号、ACL 或额外媒体容量。Host 可独立改变 code-only policy、设置/替换/移除密码,以及 rotate/revoke grant;移除私密房间的密码会回到仅邀请可入,而不会切换为公开。 -Host 的同源 `localStorage` creation profile 保存显示名、code-only policy 和可选房间密码,使进程重启或租约过期后的下一次显式分享可原子应用同一偏好。该密码只保存在 Host 浏览器;不保存在 Viewer 浏览器、URL/query/fragment、cookie、日志、错误或服务器持久存储。建房/更新请求通过 HTTPS/WSS 把明文短暂交给服务端,服务端使用 Node 标准异步 `scrypt` 固定 `N=16384,r=8,p=1`、16-byte 随机 salt 和 32-byte verifier,只在当前房间内存保存 48-byte material。进程级 gate 最多执行 2 个、等待 16 个派生,队满即有界失败;任务开始和提交前都复核当前 Host/Viewer session 与 room material。不存在、已回收或已过期房间使用 `ROOM_NOT_FOUND`;现存房间的未配置/错误密码和正确密码后的满房使用 `ROOM_ACCESS_DENIED`。 +房间准入管理统一使用同源 `POST /api/rooms/{roomId}/access`:site-access cookie 与 `Authorization: Bearer {hostToken}` 共同授权一个受 schema 约束的 policy、password、rotate 或 revoke 动作。该控制面在分享活动和休眠期间语义相同,不建立 Host participant/WebSocket/media route,不改变 `sharingActive` 或 dormant lease;rotate/revoke 的现有强撤销副作用保持不变。 + +Host 的同源 `localStorage` creation profile 保存显示名、code-only policy 和可选房间密码,使进程重启或租约过期后的下一次显式分享可原子应用同一偏好。该密码只保存在 Host 浏览器;不保存在 Viewer 浏览器、URL/query/fragment、cookie、日志、错误或服务器持久存储。建房/更新请求通过 HTTPS 把明文短暂交给服务端,服务端使用 Node 标准异步 `scrypt` 固定 `N=16384,r=8,p=1`、16-byte 随机 salt 和 32-byte verifier,只在当前房间内存保存 48-byte material。进程级 gate 最多执行 2 个、等待 16 个派生,队满即有界失败;任务开始和提交前都复核当前 Host authority 与 room material。不存在、已回收或已过期房间使用 `ROOM_NOT_FOUND`;现存房间的未配置/错误密码和正确密码后的满房使用 `ROOM_ACCESS_DENIED`。 邀请使用 `/r/{roomId}#v={grant}`。RFC 3986 要求 fragment 在 dereference 前由 user agent 分离,因此 HTTP request-target 与反向代理 access log 看不到它;WHATWG WebSocket 还明确拒绝含 fragment 的 URL。客户端只从页面 URL 严格解析一次,把 grant 写入 `screener:viewer-grant:{roomId}` 的 `sessionStorage`,随即以 `history.replaceState` 把地址栏替换为 canonical `/r/{roomId}`;刷新和页面内 WSS 重连继续使用该页面会话的值。Host 建房/rotate 收到的 raw grant 同样只在内存和 room-scoped `sessionStorage` 中保留,用它临时构造复制链接;`localStorage` 的 room ownership 只包含 canonical URL、room ID、Host token 和期限。grant 不进入 cookie、query、WebSocket URL、Referrer、日志或错误。生产继续发送 `Referrer-Policy: no-referrer`;fragment 与该 header 降低传输泄漏,但不能防止原邀请消息、浏览器扩展、截图或用户转发,UI 必须把链接称为访问凭据。 @@ -102,7 +104,7 @@ rotate/revoke 是 grant 路径的强撤销:服务端先同步提交新 digest/ Web Host 在 `authenticate` 中显式发送 `viewerPresence: true` capability,Viewer 也可显式订阅同一 participant snapshot;Native sender 不在 accepted Browser wire 内并在 authority 前拒绝。两端可在 `authenticate.displayName` 发送名称,并以 `set-display-name` 更新当前会话。服务端只向已订阅的 Web participant 发送完整 `viewer-presence` snapshot,列表严格来自当前带 sessionId 的在线参与者,不含五秒媒体恢复 grace 成员;条目用 `role` discriminator 区分 Host/Viewer。加入、离线、会话替换、改名和 active route 改变都会重发快照;该只读广播不调用 connect/reconcile,也不建立任何媒体边。Host 的“在线”Viewer 总数只取 Viewer 条目数;最多两条本机媒体快照只补充当前以 Host 为 active upstream 的连接指标。 -每个 presence 条目包含 `role`、`peerId`、`displayName` 和服务端 active assignment 的精确 `upstream`:Host 为 `none`,Viewer 为 `none | peer(peerId) | sfu`。UI 默认只展示名称;房内出现同名时,仅为同名项追加 peerId 尾部至少六字符短后缀,并只在同名项后缀碰撞时逐字符有界延长至完整 peerId。卡片状态只依据本机连接或当前 Viewer 接收证据显示“已连接”,否则保持中性的“线路分配中”或具体连接状态;active assignment 本身不是连接成功证据。连接详情同样等待当前媒体证据,再显示绿色 `P2P` 或黄色 `SFU fallback`。Host 观看者卡片采用两级指标披露:一级固定保留 route/transport、协议、帧率、分辨率、码率、丢包、RTT 和发送质量限制,codec/fmtp、编解码深指标、sender 参数与音频细节由卡片内的二级按钮按需展开;Viewer 自身的独立详情页保持完整指标。Host 与 Viewer 都可通过独立拓扑按钮从 authoritative assignment snapshot 展示 Host、实际存在的 SFU 节点及精确上下游;Viewer 默认收起并放在视频下方,没有当前 Host presence 时不生成占位拓扑。拓扑不把计划线路当作连接成功,也不显示 IP 或唯一名称对应的 peerId。peerId 不因此成为授权身份或路由输入;名称、presence 与拓扑视图不改变授权、媒体或路由状态机。 +每个 presence 条目包含 `role`、`peerId`、`displayName` 和服务端 active assignment 的精确 `upstream`:Host 为 `none`,Viewer 为 `none | peer(peerId) | sfu`。UI 默认只展示名称;房内出现同名时,仅为同名项追加 peerId 尾部至少六字符短后缀,并只在同名项后缀碰撞时逐字符有界延长至完整 peerId。卡片状态只依据本机连接或当前 Viewer 接收证据显示“已连接”,否则保持中性的“线路分配中”或具体连接状态;active assignment 本身不是连接成功证据。Host 对 direct、Peer 转发和 SFU Viewer 使用完全相同的卡片结构,不生成 SFU 分组或 Host publication 伪 Viewer。连接详情同样等待当前媒体证据,再显示绿色 `P2P` 或黄色 `SFU fallback`;每张卡优先使用该 Viewer 回报的实际接收指标,Host 直连发送快照只在尚无回报时提供同结构回退。一级固定为分辨率、帧率、码率和丢包,二级按方向只渲染当前有值的 RTT、jitter、冻结/丢帧、解码耗时、音频播放质量,或捕获/编码输入、编码耗时、编码器与质量限制。固定 VP8/Opus 不显示;实际 codec 偏离产品合同时在卡片内提示。候选地址、候选对 ID、STUN 计数、RID、fmtp、常规 sender 请求/读回和其他内部标识不渲染。Viewer 接收证据由服务端绑定当前 `viewer/upstream/connection/route revision`:Peer 子边只送达其直接父节点及 Host,SFU 接收只送达 Host;因此 SFU Viewer 继续带 Peer 子节点时,两名 Viewer 的证据仍各自归属,不逐层转发或互相冒充。Host 与 Viewer 都可通过独立拓扑按钮从 authoritative assignment snapshot 展示 Host、实际存在的 SFU 节点及精确上下游;Viewer 默认收起并放在视频下方,没有当前 Host presence 时不生成占位拓扑。拓扑不把计划线路当作连接成功,也不显示 IP 或唯一名称对应的 peerId。peerId 不因此成为授权身份或路由输入;名称、presence 与拓扑视图不改变授权、媒体或路由状态机。 ## 信令协议 @@ -112,11 +114,11 @@ WebSocket 使用无 query/fragment 的同源 `/signal`。upgrade 只记录 site- | 方向 | 消息 | 作用 | | --- | --- | --- | -| client -> server | `authenticate` | Host 以 upgrade 时的 admission 状态加 room-scoped Host token 建立会话并可订阅 Web-only presence/settings;Viewer 以 exact-room Viewer grant,或 site access 加当前 `open/password` code-only policy 建立仅观看会话,并可附带规范化本地显示名 | -| server -> client | `authenticated` | 返回角色、peerId、可为空的房间期限、容量、host 状态、该 viewer 的历史 connectionId、host 的权威 viewer 列表和 ICE 配置 | +| client -> server | `authenticate` | Host 以 upgrade 时的 admission 状态加 room-scoped Host token 建立会话并可订阅 Web-only presence/settings;Viewer 以 exact-room Viewer grant,或 site access 加当前 `open/private` code-only policy 建立仅观看会话,并可附带规范化本地显示名 | +| server -> client | `authenticated` | 返回角色、peerId、可为空的房间期限、容量、host 状态、该 viewer 的历史 connectionId、host 的权威 viewer 列表和 ICE 配置;仅 Host 额外接收是否已配置房间密码的布尔状态,不回显密码 | | viewer -> server | `set-display-name` | 只更新该 Viewer 当前 authenticated socket state 的规范化名称,不写房间或数据库状态 | | server -> subscribed Web host | `viewer-presence` | 返回全部当前在线 Viewer 的权威 snapshot 与精确 active upstream;不触发媒体连接 | -| host -> server -> host | room-access update | 独立设置 `open/password`、设置/移除 password,或 rotate/revoke grant;raw grant 只在新建或 rotate 成功响应中出现一次,服务端不回显密码 | +| Host HTTPS -> server | room-access update | 以 site access 与精确 Host token 独立设置 `open/private`、设置/移除 password,或 rotate/revoke grant;不建立媒体会话或续租,raw grant 只在新建或 rotate 成功响应中出现一次,服务端不回显密码 | | server -> host | `peer-joined` / `peer-left` | 建立或关闭该观看者的独立连接 | | host -> viewer | `offer` / `candidate` | 带 `peerId` 与 `connectionId` 的定向信令 | | viewer -> host | `answer` / `candidate` | 只能回给本房间分享者 | @@ -293,9 +295,9 @@ Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `View 首屏就是工具,不做营销页。视觉采用中性深灰工作台,绿色表示 direct、琥珀色表示 relay/警告、红色表示失败,避免单一蓝紫色主题;浏览器 favicon 复用页头 `MonitorUp` 的深绿底、绿色显示器和上行箭头语言。 - 访问入口:未鉴权根路径、`/join` 和合法 code-only `/r/{code}` 显示“站点访问”与“站点口令”;malformed `/r/...` 直接显示房间号必须是 `1000..9999` 四位数字,其他未知路径只显示“无法访问”。有效 `/r/{code}#v={grant}` 直接进入;其余 Viewer 通过 site access 后以 code-only 方式进入。未分配、已回收或已过期的 well-formed code 显示“房间不存在或已过期”,不展开或提供密码输入;现存房间的 `ROOM_ACCESS_DENIED` 保留“输入房间密码”和使用 Host 邀请的动作。已有有效 site-access cookie 不重复显示整站验证;站点口令和房间密码不共享输入值或授权状态。 -- 分享者根页面:顶部显示“{分享者显示名} 的屏幕”和房间状态,并在标题旁显示可单独复制的紧凑四位房间号;Viewer 同一位置显示权威 Host 名称,不再在标题下重复。主区域保持稳定的 16:9 舞台。未发布、已停止或可恢复错误状态时,舞台内以同等尺寸和权重显示“开始分享”和“加入房间”,中间只用“或”与分隔线表达发送/接收;固定 action row 只能包含这两个按钮及分隔,disclosure 是紧随其后的 sibling,不得参与 action-row track sizing。前者直接开始现有分享流程,后者在当前舞台内展开共享的房间码表单,提交后才导航到 `/r/{code}`。按钮同步 `aria-expanded`/`aria-controls`,展开后输入获得焦点,DOM 与焦点顺序保持触发器后接表单;窄屏允许 disclosure 自己换行,不改变两个 action 的矩形或产生横向滚动。不增加首页、向导或拓扑选择。直播时舞台显示本地预览及独立全屏图标,底部保留停止、同步暂停音视频和质量档位。邀请控制使用紧凑 copy-first 布局:包含 22 字符 grant 的完整短 URL 保持可见,复制是主动作;更新和撤销使用有 tooltip/accessible name 的 familiar icons,房间号复制、`开放 | 密码 | 关闭` code-entry segmented control 与密码编辑保持职责分离。页面失焦仅暂停本地 video element,并在舞台提示“本地预览已暂停,分享仍在继续”;媒体暂停则禁用 capture stream 轨道,两者都保留现有连接。 +- 分享者根页面:顶部显示“{分享者显示名} 的屏幕”和房间状态,并在标题旁显示可单独复制的紧凑四位房间号;Viewer 同一位置显示权威 Host 名称,不再在标题下重复。主区域保持稳定的 16:9 舞台。未发布、已停止或可恢复错误状态时,舞台内以同等尺寸和权重显示“开始分享”和“加入房间”,中间只用“或”与分隔线表达发送/接收;固定 action row 只能包含这两个按钮及分隔,disclosure 是紧随其后的 sibling,不得参与 action-row track sizing。前者直接开始现有分享流程,后者在当前舞台内展开共享的房间码表单,提交后才导航到 `/r/{code}`。按钮同步 `aria-expanded`/`aria-controls`,展开后输入获得焦点,DOM 与焦点顺序保持触发器后接表单;窄屏允许 disclosure 自己换行,不改变两个 action 的矩形或产生横向滚动。不增加首页、向导或拓扑选择。直播时舞台显示本地预览及独立全屏图标,底部保留停止、同步暂停音视频和质量档位。邀请控制使用紧凑 copy-first 布局:包含 22 字符 grant 的完整短 URL 保持可见,复制是主动作;更新和撤销使用有 tooltip/accessible name 的 familiar icons,房间号复制、`公开 | 私密` code-entry segmented control 与可选密码编辑保持职责分离。页面失焦仅暂停本地 video element,并在舞台提示“本地预览已暂停,分享仍在继续”;媒体暂停则禁用 capture stream 轨道,两者都保留现有连接。 - `/join`:保留为可直接访问的回退入口,并复用同一个四位房间码表单;一个纯 route parser 返回 `host | join | viewer | malformed-room | unknown`,只把精确 `/r/{1000..9999}` 当成 Viewer route。输入保留用户原文,`inputMode="numeric"` 只提示软键盘,提交时由共享 schema 拒绝而不是 `trim`、删除非数字或接受 Unicode 数字。通过 site access 后输入合法 code 并进入 `/r/{code}`,后续行为由 code-only admission 决定。 -- `/r/{code}` 观看者:首次解析合法 fragment grant 后立即清理地址栏并直达;仅有 code 时先通过 site access,再按 `open/password` policy 进入。鉴权后主区域最大化远端视频,标题旁显示可复制的紧凑房间号,顶部保留连接状态,并提供本地音量、静音、全屏和重试控制;Host 主动暂停时复用视频舞台遮罩显示“分享者已暂停”,恢复后立即移除。视频舞台用单一派生 presentation reducer 合成 access、Host presence、route prepare/active、peer/SFU connection、remote track、首个 composited frame、autoplay rejection 和 bounded failure。支持时用 `requestVideoFrameCallback()` 确认第一帧已提交,旧浏览器回退到当前代 decoded-frame delta 与 video readiness;`playing`、track object 或 ICE `connected` 单独都不足以结束占位层。连接阶段占用固定布局且不显示虚假百分比,播放按钮只处理实际 autoplay rejection,失败重试只调用现有有界 controller 入口。 +- `/r/{code}` 观看者:首次解析合法 fragment grant 后立即清理地址栏并直达;仅有 code 时先通过 site access,再按 `open/private` policy 进入。私密房间统一返回通用拒绝并允许尝试密码,不披露房间是否配置了密码。鉴权后主区域最大化远端视频,标题旁显示可复制的紧凑房间号,顶部保留连接状态,并提供本地音量、静音、全屏和重试控制;Host 主动暂停时复用视频舞台遮罩显示“分享者已暂停”,恢复后立即移除。视频舞台用单一派生 presentation reducer 合成 access、Host presence、route prepare/active、peer/SFU connection、remote track、首个 composited frame、autoplay rejection 和 bounded failure。支持时用 `requestVideoFrameCallback()` 确认第一帧已提交,旧浏览器回退到当前代 decoded-frame delta 与 video readiness;`playing`、track object 或 ICE `connected` 单独都不足以结束占位层。连接阶段占用固定布局且不显示虚假百分比,播放按钮只处理实际 autoplay rejection,失败重试只调用现有有界 controller 入口。 - 首帧观察绑定当前 assignment/connection/media-binding generation 以及适用的 publication generation;`srcObject` 或 route identity 变化立即使旧回调和旧 decoded baseline 失效,迟到回调不能把新路线标成已播放。 - 移动端:视频保持稳定宽高比,控制与状态移到视频下方,任何文字和按钮不得溢出或遮挡。 @@ -333,12 +335,12 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio 自动测试: - SiteAccess 的 production 必配、8 至 128 visible-ASCII bytes/独立 secret、local-dev 显式关闭、12 小时签名 cookie、过期、篡改、cookie 属性、已移除 access key fail-fast 和无服务端 session 状态; -- RoomStore 的四位空闲码随机分配/释放、活动分享免过期、dormant lease/续租、Host token/Viewer grant 摘要验证、22 字符 128-bit grant 与 exact room incarnation 同寿、`open/password` 正交 policy、跨房/同号重建拒绝、rotate/revoke 强撤销、Viewer 上限和超额拒绝; +- RoomStore 的四位空闲码随机分配/释放、活动分享免过期、dormant lease/续租、Host token/Viewer grant 摘要验证、22 字符 128-bit grant 与 exact room incarnation 同寿、`open/private` 正交 policy、私密无密码仅邀请可入、跨房/同号重建拒绝、rotate/revoke 强撤销、Viewer 上限和超额拒绝; - 进程重启后 room state 全空、旧 Host/grant/password fail closed、释放号码复用时旧 grant 仍失败;`ROOM_LEASE_SECONDS` 的默认/边界与 `MAX_ROOMS <= 9000` 校验,已删除的 `ROOM_DATABASE_PATH`/`ROOM_TTL_SECONDS` fail-fast; - STUN UDP URL 严格校验、生产 STUN 必需、LiveKit tuple 完整性/TTL/secret 独立性、当前 route/session/revision/connection generation guard,以及 ordinary peer、未授权房间和 Native 的 STUN-only isolation;LiveKit `sfu-config` 保持独立边界; - Accepted v12 contract 使用单一 `screener-v12` 信令 literal、严格 JSON schema、方向与房间授权校验、payload 上限和 client-mismatch 刷新终态;分享状态由 `shareGeneration`-fenced paused boolean 表达,missing room 使用 `ROOM_NOT_FOUND`,现存房间准入失败使用 `ROOM_ACCESS_DENIED`,Viewer route failure 为 typed terminal,Host-only bounded route snapshot 保留给自动验收但不进入产品 UI。`qualityProfileIdSchema` 保持三档,`qualityResolutionSchema` 包含 `480p`,`DEFAULT_QUALITY_SETTINGS` 为 1080p30,quality schema 严格拒绝 codec 字段; - 质量协议的 host 授权、严格字段与数值范围、last-wins、当前/未来 relay、已配置 SFU 和普通 P2P wire 兼容;sender 的四字段 requested/applied 读回、拒绝与持续受限提示;screen audio SDP helper 覆盖 CRLF、existing/missing fmtp、unknown key preservation、幂等、payload number、rejected/non-audio,以及 empty/no-Opus/ambiguous/parser 异常原对象 fallback,`ViewerPeer` 覆盖 initial/restart/rebuild 时同一结果的 local/signaling 一致性,SFU publisher 精确断言 high-quality stereo/forceStereo/DTX 且不覆盖 RED; -- HTTP/WS 集成:nginx per-source site-access `limit_req` 与错误不泄露 secret,应用不建立 IP limiter 状态;Viewer 可在 Origin/容量门下 upgrade;Host 首条鉴权同时要求 upgrade-captured site access 与 Host token,Viewer 要么持 exact-room grant,要么先有 site access 再按 `open/password`。覆盖原子创建 profile、同浏览器重建偏好、fragment/sessionStorage/replaceState、无 site access 的 code-only 拒绝、unknown/reclaimed/expired room 使用 `ROOM_NOT_FOUND` 且不显示密码、现存 wrong-or-absent-password/full code-only 使用 `ROOM_ACCESS_DENIED`、grant 与 Host 的独立错误、raw grant/密码不进入 URL/日志/服务端持久状态,以及 rotate/revoke 后 grace/route 清理、旧 Viewer/上游媒体边关闭和 reconnect 拒绝; +- HTTP/WS 集成:nginx per-source site-access `limit_req` 与错误不泄露 secret,应用不建立 IP limiter 状态;Viewer 可在 Origin/容量门下 upgrade;Host 首条鉴权同时要求 upgrade-captured site access 与 Host token,Viewer 要么持 exact-room grant,要么先有 site access 再按 `open/private`。覆盖原子创建 profile、同浏览器重建偏好、fragment/sessionStorage/replaceState、无 site access 的 code-only 拒绝、unknown/reclaimed/expired room 使用 `ROOM_NOT_FOUND` 且不显示密码、私密无密码与 wrong-or-absent-password/full code-only 使用 `ROOM_ACCESS_DENIED`、grant 与 Host 的独立错误、raw grant/密码不进入 URL/日志/服务端持久状态,以及 rotate/revoke 后 grace/route 清理、旧 Viewer/上游媒体边关闭和 reconnect 拒绝; - 客户端策略与 stats 解析:会话替换不重连,ordinary ICE 与 LiveKit protocol 分离; - SFU client isolation:`SfuPublisher` 与 `SfuSubscriber` 的 `Room.connect` mock 分别精确断言 `autoSubscribe: false` 与 `rtcConfig: { iceServers: [] }`,ordinary `HostPeer`/`ViewerPeer` 回归继续断言部署 STUN,防止空列表遗漏 Viewer 或扩散到 peer edge; - route planner property tests:候选输入乱序仍产生相同 list,健康边 sticky,硬过滤拒绝 stale/cycle/effective-capacity/unadmitted parent,优先结果深度和剩余容量并稳定打破平局;失败候选清理后 cursor 前进且总 deadline 不重置; @@ -360,7 +362,7 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio ## 当前交付边界 -- Canonical `main` 包含精确 source implementation `064db16c1b6d25fd24b7057b7e2fca172002e3c4`,使用严格单一 `screener-v12` wire,并已实现短 room-lived grant、明确 missing-room 结果、产品诊断下载移除、完整短 URL 邀请 UI 与 exact-current audio stats。Production 仍运行精确 deployed application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996`、release `c4962f5` 和严格 `screener-v11`,v12 尚未部署。源码与生产均固定 Browser VP8 且 video 不设置 `contentHint`,codec selector、quality-state/wire 字段与分享生命周期 codec 分支不存在。 +- Current source 使用严格单一 `screener-v12` wire,并已实现短 room-lived grant、明确 missing-room 结果、产品诊断下载移除、完整短 URL 邀请 UI 与 exact-current audio stats。Production 仍运行精确 deployed application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996`、release `c4962f5` 和严格 `screener-v11`,v12 尚未部署。源码与生产均固定 Browser VP8 且 video 不设置 `contentHint`,codec selector、quality-state/wire 字段与分享生命周期 codec 分支不存在。 - Pause/Resume 由当前 Host session 和 `shareGeneration` 约束。Host 后台性能和 VP8 hardware acceleration 分别按受控根因证据诊断;标准 ICE/SFU 按真实异构网络验收。 - 当前发布面只包含 Web Host、Viewer 和 browser relay。Native sender、capture helper、shared-encode executable 与两个分发包仍由各自后续决策和物理证据驱动,不属于当前 Browser release。 diff --git a/docs/需求理解.md b/docs/需求理解.md index 8fc8ac2..e71368f 100644 --- a/docs/需求理解.md +++ b/docs/需求理解.md @@ -17,7 +17,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap ### P0 - 必须实现 - 首屏双入口:分享入口通过站点访问后,主工作区必须同等突出“开始分享”和“加入房间”两个主要动作,并用清楚的视觉分隔表达发送与接收;持合法房间 grant 的观看者不需要知道站点访问密码。“加入房间”在当前舞台内就地展开房间码输入并提交,不先跳转到独立页面;直接访问 `/join` 仍提供同一输入能力。不增加营销首页、模式向导或拓扑选择。浏览器标签页使用与现有 `MonitorUp` 品牌语言一致的 favicon。 -- 简单房间:分享者从当前空闲池随机获得一个 `1000` 至 `9999` 的四位房间码,房间码只负责定位,不是秘密或永久身份。Host 与 Viewer 标题附近都紧凑、显眼地显示房间号并提供只复制房间号的动作;每个新房默认生成含独立 Viewer grant 的完整邀请链接,Host 的主分享动作复制该链接。房间号入口只有公开加入和密码加入两种状态;新房默认允许已通过站点访问的 Viewer 直接用房间号加入且不设房间密码,Host 可改为要求房间密码。邀请链接与房间号准入相互独立。所有入口只授予该房间 Viewer,不能建房、发布、冒充 Host 或跨房。 +- 简单房间:分享者从当前空闲池随机获得一个 `1000` 至 `9999` 的四位房间码,房间码只负责定位,不是秘密或永久身份。Host 与 Viewer 标题附近都紧凑、显眼地显示房间号并提供只复制房间号的动作;每个新房默认生成含独立 Viewer grant 的完整邀请链接,Host 的主分享动作复制该链接。房间号入口只有公开和私密两种状态;新房默认公开,允许已通过站点访问的 Viewer 直接用房间号加入且不设房间密码。私密且未设置密码时只有有效邀请可加入;设置密码后才额外允许房间号加密码加入。邀请链接与房间号准入相互独立。所有入口只授予该房间 Viewer,不能建房、发布、冒充 Host 或跨房。 - 免安装观看:桌面和手机观看者可直接通过浏览器链接进入、授权并播放,不依赖与分享端相同的桌面客户端。 - 屏幕采集:分享者可主动选择显示器或应用窗口,开始、停止、临时同步暂停音视频和切换分享来源;暂停保留现有连接并禁用当前 capture stream 的全部媒体轨道。服务端按当前分享代次同步这一主动暂停状态,观看端在视频舞台显示“分享者已暂停”,不得从黑帧、track mute 或网络状态推断暂停;晚加入和信令重连使用权威快照,恢复、停止、新分享或会话替换清除旧提示。Host 本地预览必须直接使用 capture stream,支持独立全屏;页面失焦或隐藏时只暂停本地 video element,并明确提示“本地预览已暂停,分享仍在继续”,恢复焦点后继续预览,不得停止 capture、编码或上行。预览不得建立 viewer/媒体边或消耗 SFU、应用服务器 media egress;预览仍可能产生本地合成、缩放和 GPU 显示成本,不能把隐藏预览称为停止采集或编码。 - 游戏声音:Windows Chrome/Edge 的初次分享和切换来源都默认请求可用的标签页、窗口或系统音频;浏览器、系统、所选来源或用户没有返回音轨时必须明确提示,而不是静默无声或阻止 video-only 分享。 @@ -33,9 +33,9 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap - 断线恢复:临时网络变化先交给 WebRTC/LiveKit 的 ICE、consent、重连和流状态。Direct edge 可在 exact current parent/child sessions 内采用 framework rebuild 的新 connection ID而不改变topology。应用只在上述exact edge失效后运行一次有界reconcile;任一步成功即停止。候选耗尽时hard-invalid、confirmed-departed或overflow edge被退休并进入明确waiting,healthy bootstrap edge保留。所有异步结果受authenticated session、share generation、base/pending route revision与candidate connection identity约束,未受影响分支保持sticky。权威暂停abort未提交candidate、保留active graph、抑制stall,并在resume后重新reconcile。 - 同一 operation 的一个总 deadline 只按实际存在的 `direct | sfu` 语义阶段等分,不配置每个 candidate 的 magic timeout。阶段内的硬失败继续推进 cursor;silent ICE 到达 direct 阶段边界时跳过剩余 direct tuple,必须把派生份额留给 SFU。20 名 Viewer 同时加入时,每名 child 的 operation deadline 不会被人数除分,但单一 room-serial operation 可能形成线性排队尾延迟;在接受另一种并发模型前,只测量 queue wait、candidate start、first decoded frame 与 final route 的 p50/p95/max,不用 NAT 猜测、TCP probe 或 magic timeout 绕过排队。 - 可观测性:按目标 RTP transport 与不透明连接/ICE 代次关联 selected path、`iceGatheringState`/ICE/connection state、事件推导的建立时长、generation-baselined pair-change delta,以及 RTT、RTP 丢包率、抖动、码率、帧率、编码耗时和受限原因。候选、route decision、队列时序与错误的 wire 和日志使用封闭 schema 和 code bucket,禁止 raw SDP、candidate、地址、URL、`errorText`、真实 peer/parent/session/connection identity 或网络标识。Host 与 Viewer 产品 UI 不提供连接诊断文件下载;本地折叠详情只显示当前页面直接观测到的连接事实。现有 Host-only bounded route snapshot 保留给自动验收,不连续推送、不进入产品 UI、也不形成 route authority。只有创建该 `RTCPeerConnection` 的本机可显示浏览器对 selected local/remote candidate 实际暴露的地址与端口;该值不上传、不持久化、不用于地理/NAT 推断,缺失或被隐私保护时显示未知。 -- 渐进式诊断:分享者和观看者默认只看到房间、画面、核心状态和常用控制;Host 的 Viewer 卡片只把本机连接或当前接收证据称为“已连接”,其余状态保持中性的“线路分配中”或具体连接状态,不把 active assignment 当作连接成功。展开连接详情也只在当前媒体证据成立后显示媒体方式:direct peer使用绿色`P2P`,SFU使用黄色`SFU fallback`。尚未连接时不预告 P2P、SFU 或未知 ICE,只保留中性状态。Host/Peer 上下游仍只在独立的权威拓扑视图中展示。传输协议、候选路径、RTT、视频/音频 RTP 区间丢包率、实际编解码器、音频区间码率/jitter 和视频编码耗时等技术详情由一个明确的“显示连接详情”开关展开。远端 Viewer evidence 的详情 shell 在同一 viewer/parent/connection/route identity 内保持稳定;非空字段从各自最后一次实际观测起精确保留五秒,`connecting`/`disconnected` 只降为非 fresh,`failed`/`closed`、identity/session/share/route 变化或非单调 sequence 则清空/换代。非 fresh evidence 不得继续证明“已连接”或被其他 current-state 消费者使用。音频 codec MIME、RTP clock、channels 与 fmtp 只能来自当前 RTP 对象引用的 `RTCCodecStats`,缺失即未知;`opus/48000/2` 不得写成 48 kHz 音源或实际立体声。隐藏诊断不能隐藏失败原因、音频缺失、持续画质受限或需要用户操作的告警。 -- 站点访问:生产部署必须配置独立的 `SITE_ACCESS_PASSWORD` 控制建房、Host 信令角色和所有 code-only Viewer 入口;只有本地开发/测试可显式关闭。非空值接受 8 至 128 个可见 ASCII bytes,不能复用 LiveKit、TLS 或其他部署 secret。`POST /api/site-access` 验证成功签发 12 小时、无服务端会话状态的 HttpOnly、`SameSite=Strict` site-access cookie。有效 room-scoped fragment grant 可绕过站点 gate 直接进入对应房间;其余 `/r/{code}` 和 `/join` 必须先持有效 site-access cookie,之后才检查房间是否存在与 `open | password` code-only policy。未分配、已回收或已过期的 code 返回 `ROOM_NOT_FOUND`;现存房间的密码与容量拒绝继续使用 `ROOM_ACCESS_DENIED`。建房 HTTP 只认该 cookie。已移除的旧环境变量、endpoint 和 cookie 不保留兼容。不引入账号、JWT、服务端 session Map 或通用 logout。 -- 房间级观看授权:新房间总是生成一个 128-bit 随机、22 字符 base64url、可 rotate/revoke 的 room-scoped Viewer grant;服务端内存只在该 exact room incarnation 保存完整 grant 的 SHA-256 摘要。grant 没有独立到期时间或内嵌 room/expiry 字段,与房间同寿,并在房间回收、进程重启、rotate 或 revoke 时失效;同号新房间使用新摘要,旧链接不能进入。浏览器从 URL fragment 严格读取 grant 后,只写该房间的 `sessionStorage` 并立即用 `history.replaceState` 清除 fragment;刷新和页面内重连可继续。code-only 入口独立采用 `open | password`,默认 `open` 且无房间密码;所有 code-only 尝试先通过站点访问。grant 不得进入 `localStorage`、cookie、query、WebSocket URL、Referrer、日志或错误。Host 可独立 rotate/revoke grant 或改变 code-only policy;授权变化必须先提交新的内存代次,再清除 Viewer grace/路由、终止旧代次信令和媒体并断开受影响 Viewer。 +- 渐进式诊断:分享者和观看者默认只看到房间、画面、核心状态和常用控制;Host 对每名 Viewer 使用同一种卡片,不按 direct、Peer 转发或 SFU 分组。卡片只把本机连接或当前 Viewer 接收证据称为“已连接”,其余状态保持中性的“线路分配中”或具体连接状态,不把 active assignment 当作连接成功。展开连接详情也只在当前媒体证据成立后显示绿色 `P2P` 或黄色 `SFU fallback`;一级只保留分辨率、帧率、码率和丢包等一眼可读的实际画面结果,二级只显示当前确有值且能解释网络、编解码或播放质量的指标。固定的 VP8/Opus 不常驻展示,只在实际协商偏离产品合同时提示;候选地址、候选对 ID、STUN 计数、RID、fmtp 和其他浏览器内部标识不进入产品 UI。Host/Peer/SFU 的精确上下游与分支结构只由独立权威拓扑视图表达,不在卡片内重复。远端 Viewer evidence 的详情 shell 在同一 viewer/upstream/connection/route identity 内保持稳定;非空字段从各自最后一次实际观测起精确保留五秒,`connecting`/`disconnected` 只降为非 fresh,`failed`/`closed`、identity/session/share/route 变化或非单调 sequence 则清空/换代。非 fresh evidence 不得继续证明“已连接”或被其他 current-state 消费者使用。隐藏诊断不能隐藏失败原因、音频缺失、持续画质受限、编码合同偏离或需要用户操作的告警。 +- 站点访问:生产部署必须配置独立的 `SITE_ACCESS_PASSWORD` 控制建房、Host 信令角色和所有 code-only Viewer 入口;只有本地开发/测试可显式关闭。非空值接受 8 至 128 个可见 ASCII bytes,不能复用 LiveKit、TLS 或其他部署 secret。`POST /api/site-access` 验证成功签发 12 小时、无服务端会话状态的 HttpOnly、`SameSite=Strict` site-access cookie。有效 room-scoped fragment grant 可绕过站点 gate 直接进入对应房间;其余 `/r/{code}` 和 `/join` 必须先持有效 site-access cookie,之后才检查房间是否存在与 `open | private` code-only policy。未分配、已回收或已过期的 code 返回 `ROOM_NOT_FOUND`;现存私密房间的无密码、缺失或错误密码及容量拒绝继续使用 `ROOM_ACCESS_DENIED`。建房 HTTP 只认该 cookie。已移除的旧环境变量、endpoint 和 cookie 不保留兼容。不引入账号、JWT、服务端 session Map 或通用 logout。 +- 房间级观看授权:新房间总是生成一个 128-bit 随机、22 字符 base64url、可 rotate/revoke 的 room-scoped Viewer grant;服务端内存只在该 exact room incarnation 保存完整 grant 的 SHA-256 摘要。grant 没有独立到期时间或内嵌 room/expiry 字段,与房间同寿,并在房间回收、进程重启、rotate 或 revoke 时失效;同号新房间使用新摘要,旧链接不能进入。浏览器从 URL fragment 严格读取 grant 后,只写该房间的 `sessionStorage` 并立即用 `history.replaceState` 清除 fragment;刷新和页面内重连可继续。code-only 入口独立采用 `open | private`,默认 `open` 且无房间密码;`private` 可不设密码并拒绝全部 code-only 尝试,设置密码后只接受匹配值。所有 code-only 尝试先通过站点访问。grant 不得进入 `localStorage`、cookie、query、WebSocket URL、Referrer、日志或错误。Host 可独立 rotate/revoke grant、改变 code-only policy 或设置/移除密码;授权变化必须先提交新的内存代次,再清除 Viewer grace/路由、终止旧代次信令和媒体并断开受影响 Viewer。 - 易失房间状态:服务器只在一个有界内存 RoomStore 中保存房间码、租期、Host token/Viewer grant 摘要、code-only policy、可选 48-byte 房间密码校验材料、参与者和控制状态。`ROOM_LEASE_SECONDS` 默认 `86400`;Host 正在分享时不回收,停止分享或 Host 断开后开始 dormant lease,同一 Host token 在期限内重新分享即续租,Viewer 活动不续租,过期后释放号码。应用进程重启时全部房间、租约、邀请和密码状态直接失效;不使用 SQLite、`ROOM_DATABASE_PATH`、schema migration、账号、用户表或服务器 session 表。 - 本地 Host 偏好:房间所有权继续使用随机 Host token,不使用 IP、UA、硬件信息或浏览器指纹。Host 浏览器在同源 `localStorage` 保存 raw Host token,以及昵称、code-only policy 和可选房间密码组成的创建偏好;服务器只在当前进程内保存 token 摘要和密码 verifier。旧房间不存在时,下一次显式分享创建新房间并原子应用本地偏好,同时生成新的四位房间号、Host token 和 Viewer grant。换设备、换浏览器或清理站点数据即不恢复。 ### P1 - 重要功能 @@ -83,7 +83,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap ## 边界与约束 - 首版不包含远程键鼠控制、公开/大规模直播、录制、社区系统或完整语音聊天;大规模分享直接使用现有直播服务。 -- `open` code-only 入口只省略房间密码,不省略站点访问;四位房间码只定位,不提供隐私保证、目录或防转发承诺。现有每房 Viewer 上限、全局/未鉴权连接上限、房间上限和中央 egress/admission cap 仍适用,任何访问方式都不能创建无界服务器 fanout。 +- `open` code-only 入口只省略房间密码,不省略站点访问;`private` 在没有房间密码时只允许 grant 邀请,在设置密码后才允许匹配的 code-only 请求。四位房间码只定位,不提供隐私保证、目录或防转发承诺。现有每房 Viewer 上限、全局/未鉴权连接上限、房间上限和中央 egress/admission cap 仍适用,任何访问方式都不能创建无界服务器 fanout。 - 房间控制面是单进程、单实例、易失内存状态;不得持久保存房间身份、租约、Host/Viewer credential、房间密码 verifier、成员、`clientId`、`peerId`、SDP、ICE candidate、IP 地址或媒体。进程重启是明确的全部房间失效边界,而不是恢复或迁移事件。 - 20-Viewer admission 是产品边界,不承诺任意机器、档位、网络或跨洲路径都达到相同画质;endpoint cap、SFU admission 和明确 wait/failure 继续保护资源。 - 浏览器规范要求每次由用户主动选择分享源,权限不能永久保存;纯网页不能保证指定游戏窗口或始终获得系统音频。 @@ -104,7 +104,8 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap - Given 本地输入不是 `1000..9999` 的四位房间码,When 用户提交或直接访问 malformed `/r/...`,Then 客户端保留原输入并直接提示“四位数字”,不发起房间鉴权、不静默删除字符。Given 默认新房,When 已通过站点访问的 Viewer 只输入合法房间码,Then 无房间密码即可在现有 Viewer/连接/egress 上限内加入,同时 Host 的主分享动作仍复制独立的 token-bearing link。 - Given 已通过 site access 的 well-formed code-only 尝试对应未分配、已回收或已过期房间,When `screener-v12` 首条信令鉴权到达,Then 服务端返回 `ROOM_NOT_FOUND`,UI 显示“房间不存在或已过期”且不提供密码重试。Given code 属于现存房间但缺失/错误密码、满房或其他已知有界 admission refusal,Then 服务端继续返回 `ROOM_ACCESS_DENIED` 并使用共享 authentication-failed close code,UI 可提供现有可选密码重试和 Host 邀请提示。Given grant 无效,Then 继续使用通用 `INVALID_TOKEN`;Host 鉴权保持独立错误,unexpected internal failure 使用不含 raw message 的 `SERVER_ERROR`。Given Viewer 持另一个房间的 grant、缺失 grant 或自行把 role 改为 Host,Then 不能建房、发布、跨房或绕过 Host token。 - Given Host rotate/revoke Viewer grant,When 新内存摘要和 authorization generation 提交,Then 服务端清除 Viewer grace/路由,命令 Viewer 关闭接收 PC/媒体并让每个 Host/relay/SFU upstream owner 关闭对应旧代次媒体边,最后关闭旧 Viewer socket;rotate 返回一次新 grant,revoke 锁定 grant 路径,code-only policy 保持独立。 -- Given Host 同源本地偏好保存了 `open | password` 和可选房间密码,When 服务器重启后该 Host 下一次显式开始分享,Then 客户端清除失效 ownership、创建新四位房间并原子重放偏好;新的 Host token 和 grant 替换旧值。Given 换浏览器、换设备或清理站点数据,Then 不恢复这些偏好或所有权。 +- Given Host 浏览器仍持有现存房间的 ownership,When 分享活动或休眠期间更新邀请、code-only policy 或房间密码,Then 同源 HTTP 管理只接受有效 site access 与该房间精确 Host token;操作不建立 Host 媒体会话、不把房间标为分享中、不申请 route/resource,也不续租 dormant lease。rotate/revoke 继续执行同一强撤销,服务端只在 rotate 成功时返回新的 raw grant。 +- Given Host 同源本地偏好保存了 `open | private` 和可选房间密码,When 服务器重启后该 Host 下一次显式开始分享,Then 客户端清除失效 ownership、创建新四位房间并原子重放偏好;新的 Host token 和 grant 替换旧值。Given 换浏览器、换设备或清理站点数据,Then 不恢复这些偏好或所有权。 - Given 应用进程重启,When 旧 Host、Viewer 或 code-only 链接再次访问,Then 所有旧房间、租约、摘要、密码 verifier 和参与者均已丢弃并 fail closed;运行时不读取数据库、迁移旧 schema 或恢复旧号码。 - Given 部署切换了不兼容的当前协议,When 仍打开的旧页面连接新服务端,Then 单一 release/protocol mismatch 边界终止该 session、停止自动重连并明确提示刷新;不得通过旧 parser、双写或双协议继续假连接。 diff --git a/src/client/components/ConnectionSelfCheck.tsx b/src/client/components/ConnectionSelfCheck.tsx deleted file mode 100644 index dae7407..0000000 --- a/src/client/components/ConnectionSelfCheck.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Check, CircleHelp, LoaderCircle, Network, X } from "lucide-react"; -import { useLayoutEffect, useRef, useState } from "react"; - -import { - runConnectionSelfCheck, - type ConnectionProbeResult, - type ConnectionSelfCheckResult, -} from "../media/connection-self-check"; - -const labels: Record = { - site: "站点", - signaling: "信令", - stun: "STUN", - sfu: "SFU/UDP", -}; - -function ProbeIcon({ result }: { result: ConnectionProbeResult }) { - if (result.status === "passed") return