test(native): gate one-viewer delivery evidence (#67)

This commit is contained in:
AAA authored and GitHub committed 2026-08-20 14:14:07 +08:00
1 parent 1e11e09261
commit 671ad6f22e
12 files changed
+1822 -23

No files matched your search

@@ -52,14 +52,67 @@ authentication failure gets one retry with the same room/token/client generation
and never another room POST. Server restart drops the room; SQLite stays at v2
and ordinary Web rooms are unchanged. No claim-confirm message, durable native token, or recovery
subsystem is added. Focused Go/TypeScript tests and static checks pass. The unused
generic probe framework is deliberately not retained.
No runtime/browser revalidation has run, so the product gate remains no-go and
this local branch is not a product availability claim.
generic probe framework is deliberately not retained. A current command consumer
may keep only the minimum stage ledger needed by the next authorized run.
The first authorized current-wire run on 2026-08-20 remains
`no-go-unclassified`. It used Windows amd64, Chrome 151, one real animated Chrome
tab, and an isolated local Node server with an in-memory RoomStore and peer
assistance disabled. Safe Chrome history booleans prove that the source and
Sender pages loaded and that no Viewer page was created. The runner stopped
after a bounded 20-second Sender-start interval without observing the combined
Sender-ready and Go source-RTP condition.
The timeout path failed to preserve its final Sender DOM/counter sample. It
therefore cannot distinguish capture selection, Host admission/create, WSS
authentication, local bridge, first encoded output, or Go ingest as the first
runtime break. None is a proven product failure. No retry, timeout adjustment,
Viewer, second Viewer, or FIFO run followed. A separately authorized rerun must
first make the stage-1 ledger unconditional and prove that timeout retains the
last sanitized checkpoint; only a passed Sender/source-RTP stage may create the
single Viewer. The product gate remains no-go and this local branch is not a
product availability claim.
The retained current command consumer appends and flushes only monotonic,
sanitized Sender-start fields. Its static one-Viewer gate now freezes one
socket/auth/connection/PeerConnection/video-track generation and the matching
Pion slot/edge generation, then revalidates that identity during media progress;
raw connection IDs never leave the page probe. Every CDP RPC and sample shares
its enclosing absolute deadline. `passed` is impossible until bounded Chrome
and native process-tree exit, listener closure, and audited deletion of the
exact task profile under system Temp all succeed; a reparse point or deletion
failure is a fixed cleanup failure. Pure checks cover retained timeout evidence,
hung samples, cross-generation rejection, cleanup-before-pass, and profile
cleanup failure.
One separately authorized second run then used that frozen gate with the same
bounded local topology. Its retained ledger proves `getDisplayMedia` request and
resolution, Host admission and room creation, Host WSS authentication, local
bridge readiness and fixed-`HIGH` config acceptance, exactly one encoder object,
and entry into the first WebCodecs output callback. It retained zero Go
`framesWritten`, source-RTP packets, and source-RTP bytes, so it stopped at
`sender-start` and never created a Viewer. No retry or threshold adjustment
followed. Cleanup completed before final failure: Chrome and Native exited, the
Node server and all three loopback ports closed, and the exact task Chrome
profile passed the non-reparse audit and was removed.
This remains `no-go-unclassified`. The frozen probe counted the first encoded
chunk before invoking the product output callback and did not record whether the
generation-bound local bridge attempted, returned from, or threw during its
binary send. Static inspection finds matching 17-byte big-endian frame contracts
on the Web and Go sides, but cannot convert that into runtime evidence. The proven
break interval is therefore after WebCodecs output callback entry and before Go
frame/source-RTP accounting, not a proven product bridge defect. The current
unrun gate now records only bounded attempt/synchronous-return/synchronous-throw
counters for the first local bridge generation and rechecks that identity at
Viewer signal, Viewer media, and final success. Pure tests reject late
replacement. A further run requires separate authorization. There is still no
Viewer, two-viewer, FIFO, or physical/hardware-encoder proof.
## Staged Revalidation
A new run requires separate authorization and must stop at the first failed
stage while retaining a bounded final-negative snapshot:
stage while synchronously retaining a bounded final-negative snapshot:
1. Prove host room/auth, local config acknowledgement, one encoder object,
bridge ingress, frame decode, source RTP, and zero critical errors.
+1 -2
View File
@@ -23,7 +23,7 @@ Last updated: 2026-08-20
- Use direct host P2P for one or two viewers. Keep ADR-0004 off broadly; use an isolated exact-room candidate until resource, quality, recovery, SFU/UDP, bounded-failure, and browser/mobile gates pass.
- Browser relays resend remote `MediaStreamTrack` values and re-encode at each hop; WebRTC does not guarantee a shared encoder across peer connections, so measure the cost.
- Keep experiments bounded: the standard representation sequence is below; native RTP relay, encoded-object striping, and FEC remain separate.
- ADR-0006 stays no-go. Its v2 room is memory-only, capped by `ROOM_TTL` and rearmed 300s after Host disconnect; SQLite/Web unchanged. Pre-auth retries once in-room without another POST.
- ADR-0006 stays no-go: the second run reached first WebCodecs output, then zero Go frame/RTP and no Viewer; cleanup passed 5/5. Its unrun gate rechecks one Viewer/Pion/bridge generation and cleanup-before-pass. No product bug or further run is authorized; Native v2 stays memory-only and leaves SQLite/Web unchanged.
- Mobile Web Host is unsupported; feature-detect and fail clearly. Mobile Viewer stays leaf-only. After Windows native, gate Android 14+; iOS waits for stable iOS 27 ScreenCaptureKit.
- ADR-0005 accepts direct-first Peer ICE and bounded SFU roots. Its default-off exact-room slice is undeployed; production/coturn stay STUN-only. PR #12/selected-edge superseded.
- Keep the controller exact-room only: room `1` is the STUN/SFU smoke. Preserve sticky progressing P2P, mobile leaves and break-before-make. Recovery spends one attempt per layer (ICE restart, same-parent rebuild, alternate peer, then SFU), never three identical retries; active SFU gets one fresh grant before Peer failback.
@@ -48,7 +48,6 @@ Last updated: 2026-08-20
- Production logs show two root participants and two short Host participants with no service restart or retained track. Their timing is consistent with, but does not directly prove, one fresh-grant retry and Peer failback. The Host exposes only the local current-revision failure stage (`connect|source|video-publish|sender-config|audio-publish|transport`) and uploads no raw error or endpoint data. Browser relays still re-encode; native capture/hardware remains outside validated production behavior.
- The retained v1 backup is the only schema rollback path. A failed artifact attempt exposed unsafe hard-linked dependency reuse; the successful immutable release has zero shared regular-file inodes with rollback, and future releases preserve that boundary.
- Chrome 151/LiveKit localhost A/B cut failure-to-active/render from 1.481/2.257 seconds to 0.200/0.320; 31 new frames and 25 ms sampling kept host edges at two. It is headless synthetic 720p30 and includes SDK/network prewarm, not public-network evidence.
- Source `main` at `f5d48ed` includes a memory-only Native v2 candidate, but production predates it and has no Native runtime gate. Render, hardware, two-edge/FIFO and TURN evidence remain absent.
- Chrome 151 synthetic topology/quality-control runs kept fanout 2/1 and all viewers decoding; one relay close recovered in 5.32 seconds. This is control evidence only.
- Room `1` publishes `HIGH+LOW` with Dynacast off and subscriber `HIGH` ceilings, and now has bounded local quality reparenting. Both remain unverified on real media; test zero-child leaves and calibrated C+B loss before broad rollout.
- Generation-bound C+B needs three hard-bad windows; parent samples are one-use per connection. Pending SFU binds intent/guard. Stale/successful peer work releases quality exclusion; real failure may take it over. It uploads no raw endpoint metadata and computes no score.
+95 -8
View File
@@ -4,7 +4,7 @@
- Scope: one Windows game-capture sender, one encoded video stream, and at most
two independent standard WebRTC media edges
- Status: stacked Draft PRs #16/#18/#22/#23/#25/#28 pass one bounded live
two-leg WebCodecs/Pion candidate. The first product-wiring gate is
two-leg WebCodecs/Pion candidate. Both product-wiring attempts remain
no-go-unclassified; product and physical shared encoding remain unproven.
## Decision Input
@@ -178,12 +178,93 @@ retry without a second room POST. SQLite remains v2 and ordinary Web creation is
unchanged.
HTTP statuses, Cookie attributes, create-room responses, Host authentication,
and ignored Viewer evidence are strict and bounded; errors retain only fixed categories.
Focused Go and TypeScript checks pass, but no Node/Chrome runtime gate has run.
The overbroad unused gate/probe framework was deleted; a future authorized run
must add only its executable minimum. This static checkpoint does not change the
no-go result below.
Focused Go and TypeScript checks pass. The overbroad unused gate/probe framework
stays deleted; the current command consumer is limited to the executable runner
and sanitized stage ledger required by the next authorized run.
The sole authorized 2026-08-19 local product gate is
The one authorized 2026-08-20 current-wire attempt used Chrome 151 on Windows
amd64, a temporary non-default Chrome profile, a real animated Chrome tab, and
an isolated local Node server with an in-memory RoomStore and peer assistance
disabled. It did not contact production or SQLite. Retained history booleans
show that the source and Sender pages loaded and no Viewer page was created. The
runner reported `failedStage=node-host` after its bounded 20-second Sender-start
interval because it did not observe the combined Sender-ready and Go source-RTP
condition.
This is `no-go-unclassified`, not evidence that the Node Host path itself
failed. The timeout branch did not retain a final Sender DOM/counter sample, so
it cannot locate the break among `getDisplayMedia` request/resolve, Host
admission/create, WSS authentication, local bridge connection, first encoded
chunk, or Go source RTP. There was no retry or timeout adjustment. Viewer auth,
offer/answer/ICE, Pion bound-edge output, Viewer inbound/decode/render, two-edge,
and FIFO evidence were never attempted. A later run needs separate authorization
and an unconditional stage-1 final-negative ledger before it may create one
Viewer.
That local-only runner is now the current `npm run gate:native-one-viewer`
consumer. It requires an explicit Chrome executable environment path and uses
the task-local Go executable or task-specific `PATH`; neither path is committed.
Each monotonic Sender-start transition appends and flushes one bounded JSON
record containing only booleans and capped counters. A pure timeout test proves
that capture, admission/create, Host WSS, bridge, fixed-`HIGH`, first encoded
chunk, Go-ingest, and source-RTP state cannot be erased by a later failed sample.
The one-Viewer signaling/media waits likewise preserve their latest sanitized
sample in the final report. They now accept evidence only from one frozen
signaling socket/auth/opaque-connection ordinal, one PeerConnection/video-track
ordinal, and the same Pion slot/edge generation across signal and media stages.
The raw connection ID remains page-local. Every CDP RPC/sample is capped by the
remaining stage deadline. Final status is written only after bounded process-tree
exit and closed-port polling, followed by an exact system-Temp path audit,
recursive reparse-point rejection, profile deletion, and absence verification;
any cleanup failure is fail-closed. Microsoft documents `/T` as terminating a
task's child processes and `ReparsePoint` as the filesystem attribute used for
these special entries. Pure tests cover hung sampling, cross-PC evidence,
cleanup-before-pass, profile path bounds, and deletion failure. Sources were
rechecked 2026-08-20.
One separately authorized second current-wire run used the frozen gate once and
did not retry or adjust a threshold. Its append-and-flush ledger reached sequence
7 and retained positive booleans for capture request/resolution, Host admission,
private room creation, Host WSS authentication, bridge readiness, fixed VP8
1280x720@30/3 Mbps config acceptance, one encoder object, and entry into the
first WebCodecs output callback. Go diagnostics remained at zero
`framesWritten`, source-RTP packets, and source-RTP bytes. The runner therefore
failed closed at `sender-start`; it never created the Viewer page and did not
exercise Viewer auth, SDP/ICE, Pion bound-edge output, decode/render, a second
Viewer, or FIFO. Cleanup was complete before the report was finalized: Chrome
and Native exited, the isolated Node listener and all three random loopback
ports closed, and the exact task profile passed a non-reparse audit and was
removed.
The result narrows but does not classify the break. The gate increments its
encoded-output observation before it calls the product output callback, and that
run did not retain whether the current local media WebSocket attempted, returned
from, or threw during the binary frame send. Static source comparison shows a
matching binary message type, 17-byte big-endian header, one-MiB payload limit,
and VP8 frame decode on both sides. It also confirms that Pion's unbound static RTP track
returns no error. Those facts do not prove which runtime operation followed the
callback. The retained first break interval is after WebCodecs output callback
entry and before Go frame/source-RTP accounting. There is no uniquely proven
product bug. The current unrun probe now binds the first actual local media
WebSocket as gate-local generation 1 and records only bounded binary-send
attempt, synchronous-return, and synchronous-throw counters. A second bridge
generation saturates at 2, stops accumulating the first generation's counters,
and fails the Viewer signal, Viewer media, and final-success checks even after
Sender-start passed. A pure in-memory test covers success, throw, and late
replacement without retaining a URL, payload, token, SDP, IP address, or raw
error. No subsequent browser/media run is authorized or has occurred.
Chromium documents the tab-capture auto-selection switch as a test-only aid,
and Chrome requires a non-default user-data directory for remote debugging from
Chrome 136. Those constraints explain the isolated harness setup; they do not
prove that `getDisplayMedia` resolved in the first failed current-wire run.
Static inspection of the pinned Pion v4.2.18 `TrackLocalStaticRTP.WriteRTP` shows
that it iterates the
current bindings and returns no error for an empty binding set. This excludes a
pre-Viewer no-binding write as the likely fatal break; it is not runtime proof
of Go ingest or source RTP. Sources were rechecked 2026-08-20.
The earlier authorized 2026-08-19 local product gate is
`no-go-unclassified`:
| Checkpoint | Retained evidence |
@@ -309,8 +390,9 @@ diversity, lifecycle, and sustained CPU/GPU,
memory, latency, quality, and upload measurements. A third host edge, custom
RTP/SRTP, or a custom congestion-control framework remains out of scope. The
#28 minimum-of-two policy is also out of scope for product code; retaining its
evidence does not retain its policy. Selected-edge TURN is a separate future
contract and gate, not a prerequisite silently added to this STUN-only canary.
evidence does not retain its policy. Built-in exact-room Peer ICE TURN
supersedes selected-edge as a separate Web source canary; Native stays STUN-only
unless its own bounded capability and transport gate is accepted.
## Primary Sources And License Boundary
@@ -328,6 +410,7 @@ contract and gate, not a prerequisite silently added to this STUN-only canary.
- [WebRTC video codec requirements, RFC 7742](https://www.rfc-editor.org/rfc/rfc7742.html)
- [WebRTC congestion control requirements, RFC 8836](https://www.rfc-editor.org/rfc/rfc8836.html)
- [Pion track fanout implementation](https://github.com/pion/webrtc/blob/main/track_local_static.go)
- [Pion v4.2.18 pinned track implementation](https://github.com/pion/webrtc/blob/v4.2.18/track_local_static.go)
- [Pion send-side bandwidth estimator](https://github.com/pion/interceptor/blob/main/pkg/gcc/send_side_bwe.go)
- [Pion WebRTC license](https://github.com/pion/webrtc/blob/main/LICENSE)
- [WebCodecs](https://www.w3.org/TR/webcodecs/)
@@ -337,6 +420,10 @@ contract and gate, not a prerequisite silently added to this STUN-only canary.
- [W3C WebRTC simulcast](https://www.w3.org/TR/webrtc/#simulcast-functionality)
- [Pion WebRTC v4.2.18](https://github.com/pion/webrtc/tree/v4.2.18)
- [Pion Interceptor v0.1.47](https://github.com/pion/interceptor/tree/v0.1.47)
- [Chromium Chrome test switches](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/chrome/common/chrome_switches.cc)
- [Chrome remote-debugging profile requirement](https://developer.chrome.com/blog/remote-debugging-port)
- [Microsoft `taskkill`](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/taskkill)
- [Microsoft `FileAttributes.ReparsePoint`](https://learn.microsoft.com/en-us/dotnet/api/system.io.fileattributes?view=net-10.0)
- [RFC 4585 RTP/AVPF feedback](https://www.rfc-editor.org/rfc/rfc4585.html)
- [RFC 4588 RTP retransmission](https://www.rfc-editor.org/rfc/rfc4588.html)
- [RFC 8888 congestion-control feedback](https://www.rfc-editor.org/rfc/rfc8888.html)
+8 -8
View File
@@ -11,7 +11,7 @@ Ordinary ICE is STUN-only process-wide; persistent room `1` alone enables the
automatic peer/SFU-UDP controller with at most two SFU roots. The old
`05f98d10ecd1` is the immediate rollback with the current v2 DB/env. Crossing
the admission cutover requires `89e6d7649169` plus its recorded old environment;
only deeper pre-access `9610032` may restore v1 state. Source `main` at `f5d48ed`
only deeper pre-access `9610032` may restore v1 state. Source `main` at `a11a73d`
also contains a no-go Native v2 candidate, but production does not run it.
## Current Snapshot
@@ -19,7 +19,7 @@ also contains a no-go Native v2 candidate, but production does not run it.
- Capture precedes room creation; source/quality changes preserve peers and pause keeps audio/connections. Quality defaults to `maintain-resolution` with balanced/fluid options.
- Production requires `HOST_ADMISSION_PASSWORD` only for creation/Host role and uses `screener-v2`, default private fragment grants, explicit public-watch, and generation-bound rotate/revoke. Viewers never need the Host secret; there is no account/session table or old parser.
- SQLite v2 keeps Host and nullable Viewer-grant digests in one checked `STRICT` row. The stopped migration retained four rooms and locked each old room private without minting a raw grant.
- Source-only Native v2 uses memory-only rooms, 300s post-disconnect reclaim and `ROOM_TTL`; it has no production gate.
- Source-only Native v2 uses memory-only rooms, 300s reclaim and `ROOM_TTL`; production does not run it.
- PR #44's exact-room STUN-only/SFU-UDP router is enabled only for production room `1`: host/SFU roots <=2, browser relay <=1, bounded failure. HTTPS/WSS stays TLS/TCP.
- Fallback prewarm is token-free and limited to room `1`; mobile/iPad viewers are leaves and healthy edges stay sticky.
- After an answer, a generation-bound 15s initial-connect deadline enters ICE restart; success/replacement/disposal cancels it. It is deployed but not mobile-verified.
@@ -37,7 +37,7 @@ also contains a no-go Native v2 candidate, but production does not run it.
- Host A+B and authenticated P2P Viewer C are sanitized, generation-bound, read-only, and fail closed for stale, ambiguous, or SFU-fed evidence; no raw media metadata is retained.
- SVC is `no-go-web-svc-cross-path-hardware-contract`: no direct/peer selection, cross-PC shared encode, or portable hardware proof; pinned screen share is `L1T3`. No browser run was warranted.
- Source TURN tests pass; production/coturn stay STUN-only and untested.
- Draft #16/#18/#22/#23/#25/#28 passed isolated experiments only. ADR-0006 reached host setup/one encoder output, then timed out before viewer 1 decoded/rendered.
- Native remains experimental. The second 2026-08-20 run reached first WebCodecs output, then zero Go frame/RTP and no Viewer; cleanup passed 5/5. No product bug or retry followed.
- Access focused tests pass protocol/config/HTTP/storage/RoomStore/SQLite/signaling, including commit-first teardown, persistence-failure continuity, relay/SFU retirement, grant bounds, and v1 migration rollback.
## Unverified Boundaries
@@ -47,7 +47,7 @@ also contains a no-go Native v2 candidate, but production does not run it.
- Silent partitions can wait 30 to 60 seconds for heartbeat detection before the default 5-second grace; this remains unverified.
- Real audio and heterogeneous clients remain unverified. Production reports poor film audio and voice-call self-echo under system capture; there is no app audio ceiling or Web process isolation. Diagnose A/B/C and sync, then gate Windows 11 game-process audio; Windows 10 stays unresolved without system fallback.
- The former release reportedly sustained bandwidth-limited blur on a capable LAN; Host refresh recovered while Viewer refresh did not. The new deployment has not yet reproduced or cleared it.
- ADR-0006 stays no-go: its v2 memory-only candidate passed static checks only; render, two-edge/FIFO and later TURN remain unverified.
- ADR-0006 stays no-go: its runtime gap is WebCodecs output -> Go frame/RTP; the unrun bridge-generation probe rechecks signal/media/final. Viewer, two-edge/FIFO, and Native TURN remain unverified.
- Browser fanout is host two/viewer one; any accepted endpoint relay stays capped at two downstream edges.
- PR #49/#50 BWE is unverified/default-off: test zero-child leaves, then root impact. No local per-leaf UDP shaper; resume with Linux `tc` or public canary, never CDP. Web P2P/SVC shortcuts remain no-go.
- The corrected standby smoke is localhost/headless/video-only; public DNS/TLS reuse, transport, audio, shaping, load, mobile, endurance, and sub-25 ms overlap remain open.
@@ -62,16 +62,16 @@ The recovery target uses one ICE restart, one same-parent rebuild, one alternate
SFU; it never runs three identical retries or abandons progressing P2P early.
Next gate exactly-two/Dynacast-off BWE on a zero-child SFU leaf, then root
evacuation. Native v2 remains no-go pending an authorized runtime gate; Audio
A/B/C may proceed without displacing P0.
evacuation. Native needs separate authorization; its next run gates Go RTP on
the first bridge send.
Audio A/B/C may proceed without displacing P0.
Peer ICE TURN supersedes selected-edge but remains source-only pending an isolated coturn canary.
ADR-0004 still requires a full-resolution 30-minute `1/3/5/8` network,
resource, quality, latency, recovery, and browser/mobile-leaf matrix. A separate
20-viewer gate must pass before the current default eight changes to target 20.
ADR-0004 fails closed. Native shared encode and packet/layer striping remain
separate experiments, not ways to relabel a failed browser route.
ADR-0004 remains fail-closed; Native/striping experiments cannot relabel it.
## Blockers And Decisions
@@ -0,0 +1,31 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
"github.com/TNTcraftHIM/Screener/native/sender/internal/app"
)
func main() {
sender, err := app.New()
if err != nil {
fmt.Fprintln(os.Stderr, "sender initialization failed")
os.Exit(1)
}
defer sender.Close()
launchURL, err := sender.Start()
if err != nil {
fmt.Fprintln(os.Stderr, "sender startup failed")
os.Exit(1)
}
if err = json.NewEncoder(os.Stdout).Encode(map[string]string{"launchUrl": launchURL}); err != nil {
fmt.Fprintln(os.Stderr, "sender handoff failed")
os.Exit(1)
}
_, _ = bufio.NewReader(os.Stdin).ReadByte()
}
+1
View File
@@ -14,6 +14,7 @@
"build:server": "tsc -p tsconfig.server.json",
"start": "node dist/server/server/index.js",
"benchmark:peer-assisted": "tsx scripts/peer-assisted-benchmark.ts",
"gate:native-one-viewer": "tsx scripts/native-one-viewer-gate.ts",
"typecheck": "tsc -b --pretty false",
"test": "vitest run",
"test:watch": "vitest",
+176
View File
@@ -0,0 +1,176 @@
import { basename, dirname, resolve } from "node:path";
export interface ViewerIdentityEvidence {
socketCount: number;
socketOrdinal: number;
authGeneration: number;
connectionCount: number;
connectionOrdinal: number;
pcCount: number;
pcOrdinal: number;
trackCount: number;
trackOrdinal: number;
consistent: boolean;
}
export interface SenderIdentityEvidence {
peerCount: number;
maxPeers: number;
slot: number;
edgeGeneration: number;
}
export interface BridgeSendEvidence {
bridgeGeneration: number;
binarySendAttempts: number;
binarySendSucceeded: number;
binarySendFailed: number;
}
export interface OneViewerIdentity {
socketOrdinal: number;
authGeneration: number;
connectionOrdinal: number;
pcOrdinal: number;
trackOrdinal: number;
pionSlot: number;
pionEdgeGeneration: number;
}
export interface CleanupResult {
browserExited: boolean;
nativeExited: boolean;
serverClosed: boolean;
portsClosed: boolean;
profileRemoved: boolean;
}
interface FinalizableReport {
status: "passed" | "failed";
failedStage: string | null;
stages: Record<string, Record<string, boolean | number | string | null> | undefined>;
}
export function captureOneViewerIdentity(
viewer: ViewerIdentityEvidence,
sender: SenderIdentityEvidence,
): OneViewerIdentity | null {
if (!viewer.consistent || viewer.socketCount !== 1 || viewer.authGeneration !== 1 ||
viewer.connectionCount !== 1 || viewer.pcCount !== 1 || viewer.trackCount !== 1 ||
sender.peerCount !== 1 || sender.maxPeers > 1 || sender.edgeGeneration !== 1 ||
viewer.socketOrdinal !== 1 || viewer.connectionOrdinal !== 1 || viewer.pcOrdinal !== 1 ||
viewer.trackOrdinal !== 1 || sender.slot <= 0) return null;
return {
socketOrdinal: viewer.socketOrdinal,
authGeneration: viewer.authGeneration,
connectionOrdinal: viewer.connectionOrdinal,
pcOrdinal: viewer.pcOrdinal,
trackOrdinal: viewer.trackOrdinal,
pionSlot: sender.slot,
pionEdgeGeneration: sender.edgeGeneration,
};
}
export function retainsOneViewerIdentity(
baseline: OneViewerIdentity,
viewer: ViewerIdentityEvidence,
sender: SenderIdentityEvidence,
): boolean {
const current = captureOneViewerIdentity(viewer, sender);
return current !== null && Object.keys(baseline).every(
(key) => current[key as keyof OneViewerIdentity] === baseline[key as keyof OneViewerIdentity],
);
}
export function retainsFirstBridgeSend(evidence: BridgeSendEvidence): boolean {
return evidence.bridgeGeneration === 1 && evidence.binarySendAttempts > 0 &&
evidence.binarySendSucceeded > 0 && evidence.binarySendFailed === 0;
}
export function withDeadline<T>(
operation: () => Promise<T>,
deadline: number,
now: () => number = Date.now,
): Promise<T> {
const remaining = Math.max(0, deadline - now());
return new Promise<T>((resolveValue, rejectValue) => {
const timeout = setTimeout(() => rejectValue(new Error("Operation deadline exceeded")), remaining);
Promise.resolve().then(operation).then(
(value) => { clearTimeout(timeout); resolveValue(value); },
(error: unknown) => { clearTimeout(timeout); rejectValue(error); },
);
});
}
type JsonFetch = (
input: string,
init: { signal: AbortSignal },
) => Promise<{ ok: boolean; json(): Promise<unknown> }>;
export async function fetchJsonBefore<T>(
url: string,
deadline: number,
fetcher: JsonFetch = fetch,
): Promise<T> {
const controller = new AbortController();
const abort = setTimeout(() => controller.abort(), Math.max(0, deadline - Date.now()));
try {
const response = await withDeadline(
() => fetcher(url, { signal: controller.signal }),
deadline,
);
if (!response.ok) throw new Error("HTTP response was not successful");
return await withDeadline(() => response.json() as Promise<T>, deadline);
} finally {
clearTimeout(abort);
controller.abort();
}
}
export async function waitForSample<T>(
sample: (deadline: number) => Promise<T>,
retainAndAccept: (value: T) => boolean,
timeoutMs: number,
pollMs = 100,
): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
let value: T | undefined;
try { value = await withDeadline(() => sample(deadline), deadline); } catch {}
if (value !== undefined && retainAndAccept(value)) return;
await new Promise((resolveDelay) => setTimeout(resolveDelay, Math.min(pollMs, Math.max(0, deadline - Date.now()))));
}
throw new Error("Bounded browser stage timed out");
}
export async function finalizeGate(
report: FinalizableReport,
gateSucceeded: boolean,
cleanup: () => Promise<CleanupResult>,
): Promise<void> {
let result: CleanupResult;
try {
result = await cleanup();
} catch {
result = { browserExited: false, nativeExited: false, serverClosed: false,
portsClosed: false, profileRemoved: false };
}
const complete = Object.values(result).every(Boolean);
report.stages.cleanup = { ...result, complete };
if (gateSucceeded && complete) {
report.status = "passed";
report.failedStage = null;
} else if (!complete) {
report.status = "failed";
report.failedStage = "cleanup";
}
}
export function isExactGateProfile(profile: string, systemTemp: string): boolean {
const candidate = resolve(profile);
const temp = resolve(systemTemp);
const same = process.platform === "win32"
? dirname(candidate).toLowerCase() === temp.toLowerCase()
: dirname(candidate) === temp;
return same && /^screener-native-one-viewer-[A-Za-z0-9_-]{6}$/.test(basename(candidate));
}
+977
View File
@@ -0,0 +1,977 @@
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { randomBytes } from "node:crypto";
import { closeSync, fsyncSync, openSync, writeFileSync } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { createConnection, createServer } from "node:net";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import WebSocket from "ws";
import { createScreenerServer, type ScreenerServer } from "../src/server/app";
import { loadConfig } from "../src/server/config";
import {
RoomStore,
type ConnectedParticipant,
type ConnectParticipantInput,
type RoomStoreOptions,
} from "../src/server/room-store";
import {
SenderStartLedger,
waitForSenderStart,
type SenderStartObservation,
} from "./native-one-viewer-ledger";
import {
captureOneViewerIdentity,
fetchJsonBefore,
finalizeGate,
isExactGateProfile,
retainsFirstBridgeSend,
retainsOneViewerIdentity,
waitForSample,
withDeadline,
type CleanupResult,
type OneViewerIdentity,
type SenderIdentityEvidence,
type ViewerIdentityEvidence,
} from "./native-one-viewer-gate-core";
type Stage = "preflight" | "sender-start" | "node-host" | "viewer-signal" | "viewer-media" | "cleanup";
interface PageHandle {
sessionId: string;
}
interface CdpResult<T> {
result: { value?: T };
exceptionDetails?: unknown;
}
interface GateReport {
schemaVersion: 1;
status: "passed" | "failed";
failedStage: Stage | null;
stages: Record<string, Record<string, boolean | number | string | null> | undefined>;
limitations: string[];
}
interface SenderSnapshot {
getDisplayMediaRequested: boolean;
getDisplayMediaResolved: boolean;
ready: number;
configAccepted: number;
encoderInstances: number;
encoderOutputs: number;
encoderErrors: number;
fatalEvents: number;
bridgeGeneration: number;
binarySendAttempts: number;
binarySendSucceeded: number;
binarySendFailed: number;
activeViewers: number;
maxPeers: number;
config: {
codec: string | null;
width: number | null;
height: number | null;
fps: number | null;
bitrate: number | null;
encoderInstances: number | null;
} | null;
media: {
framesWritten: number;
sourceRtpPacketsWritten: number;
sourceRtpBytesWritten: number;
} | null;
peers: Array<{
slot: number;
edgeGeneration: number;
connectionState: string;
iceConnectionState: string;
route: string;
packetsSent: number;
bytesSent: number;
}>;
}
interface ViewerSnapshot {
signaling: {
authenticateSent: number;
authenticatedReceived: number;
offersReceived: number;
answersSent: number;
candidatesReceived: number;
candidatesSent: number;
};
identity: ViewerIdentityEvidence;
connectionState: string;
iceConnectionState: string;
packetsReceived: number;
bytesReceived: number;
framesDecoded: number;
videoReadyState: number;
videoWidth: number;
videoHeight: number;
videoCurrentTime: number;
renderedFrames: number;
}
class CdpConnection {
private nextId = 1;
private readonly pending = new Map<number, {
resolve: (value: unknown) => void;
reject: (error: Error) => void;
timeout: NodeJS.Timeout;
}>();
private constructor(private readonly socket: WebSocket) {
socket.on("message", (raw) => this.onMessage(raw.toString()));
socket.on("close", () => this.closePending());
socket.on("error", () => this.closePending());
}
static async connect(url: string, deadline: number): Promise<CdpConnection> {
const socket = new WebSocket(url, { maxPayload: 16 * 1024 * 1024 });
try {
await withDeadline(() => new Promise<void>((resolveOpen, rejectOpen) => {
socket.once("open", resolveOpen);
socket.once("error", rejectOpen);
}), deadline);
} catch (error) {
socket.terminate();
throw error;
}
return new CdpConnection(socket);
}
call<T>(method: string, params: Record<string, unknown>, sessionId: string | undefined, deadline: number): Promise<T> {
const id = this.nextId++;
return new Promise<T>((resolveCall, rejectCall) => {
const remaining = Math.max(0, deadline - Date.now());
const timeout = setTimeout(() => {
this.pending.delete(id);
rejectCall(new Error("CDP command timed out"));
}, remaining);
this.pending.set(id, {
resolve: (value) => { clearTimeout(timeout); resolveCall(value as T); },
reject: (error) => { clearTimeout(timeout); rejectCall(error); },
timeout,
});
this.socket.send(JSON.stringify({ id, method, params, sessionId }));
});
}
close(): void {
this.socket.close();
}
private onMessage(raw: string): void {
let message: { id?: number; result?: unknown; error?: unknown };
try {
message = JSON.parse(raw) as typeof message;
} catch {
return;
}
if (message.id === undefined) return;
const pending = this.pending.get(message.id);
if (!pending) return;
this.pending.delete(message.id);
if (message.error) {
pending.reject(new Error("CDP command failed"));
} else {
pending.resolve(message.result ?? {});
}
}
private closePending(): void {
for (const pending of this.pending.values()) {
clearTimeout(pending.timeout);
pending.reject(new Error("CDP connection closed"));
}
this.pending.clear();
}
}
const root = resolve(import.meta.dirname, "..");
class ObservedRoomStore extends RoomStore {
constructor(options: RoomStoreOptions, private readonly onHostAuthenticated: () => void) {
super(options);
}
override connectParticipant(input: ConnectParticipantInput): ConnectedParticipant {
const participant = super.connectParticipant(input);
if (input.role === "host") this.onHostAuthenticated();
return participant;
}
}
function observeNodeStages(server: ScreenerServer, ledger: SenderStartLedger): void {
server.httpServer.prependListener("request", (request, response) => {
const path = new URL(request.url ?? "/", "http://gate.invalid").pathname;
if (request.method === "POST" && path === "/api/host-admission") {
response.once("finish", () => {
if (response.statusCode === 200) ledger.record({ hostAdmissionAccepted: true });
});
} else if (request.method === "POST" && path === "/api/rooms") {
response.once("finish", () => {
if (response.statusCode === 201) ledger.record({ roomCreated: true });
});
}
});
}
function senderStartObservation(snapshot: SenderSnapshot): Partial<SenderStartObservation> {
return {
getDisplayMediaRequested: snapshot.getDisplayMediaRequested,
getDisplayMediaResolved: snapshot.getDisplayMediaResolved,
bridgeConnected: snapshot.ready > 0,
fixedHighConfigured: snapshot.configAccepted > 0 &&
snapshot.config?.codec === "vp8" && snapshot.config.width === 1280 &&
snapshot.config.height === 720 && snapshot.config.fps === 30 &&
snapshot.config.bitrate === 3_000_000 && snapshot.config.encoderInstances === 1,
firstEncodedChunk: snapshot.encoderOutputs > 0,
bridgeGeneration: snapshot.bridgeGeneration,
binarySendAttempts: snapshot.binarySendAttempts,
binarySendSucceeded: snapshot.binarySendSucceeded,
binarySendFailed: snapshot.binarySendFailed,
encoderInstances: snapshot.encoderInstances,
framesWritten: snapshot.media?.framesWritten ?? 0,
sourceRtpPacketsWritten: snapshot.media?.sourceRtpPacketsWritten ?? 0,
sourceRtpBytesWritten: snapshot.media?.sourceRtpBytesWritten ?? 0,
};
}
function requiredEnvironmentPath(name: string): string {
const value = process.env[name]?.trim();
if (!value) throw new Error(`${name} is required`);
return value;
}
function appendLedgerRecord(path: string, record: object): void {
const descriptor = openSync(path, "a");
try {
writeFileSync(descriptor, `${JSON.stringify(record)}\n`, { encoding: "utf8" });
fsyncSync(descriptor);
} finally {
closeSync(descriptor);
}
}
async function main(): Promise<void> {
const chromePath = requiredEnvironmentPath("SCREENER_NATIVE_GATE_CHROME");
const goPath = process.env.SCREENER_NATIVE_GATE_GO?.trim() || "go";
const accessKey = randomBytes(9).toString("base64url");
const report: GateReport = {
schemaVersion: 1,
status: "failed",
failedStage: "preflight",
stages: {},
limitations: [
"The source is a real animated Chrome tab captured through getDisplayMedia, not a game workload.",
"This run proves one WebCodecs object, not one physical or hardware encoder.",
"The loopback run does not prove public STUN, restrictive networks, TURN, audio, packaging, or endurance.",
],
};
let server: ScreenerServer | null = null;
let native: ChildProcessWithoutNullStreams | null = null;
let chrome: ChildProcessWithoutNullStreams | null = null;
let cdp: CdpConnection | null = null;
let profile: string | null = null;
let ledgerPath: string | null = null;
let appPort: number | null = null;
let debugPort: number | null = null;
let nativePort: number | null = null;
let gateSucceeded = false;
const senderLedger = new SenderStartLedger((record) => {
if (!ledgerPath) return;
appendLedgerRecord(ledgerPath, record);
});
try {
appPort = await reservePort();
debugPort = await reservePort();
profile = await mkdtemp(join(tmpdir(), "screener-native-one-viewer-"));
ledgerPath = join(profile, "sender-start-ledger.jsonl");
writeFileSync(ledgerPath, "", { encoding: "utf8", flag: "wx" });
const baseUrl = `http://127.0.0.1:${appPort}`;
const config = loadConfig({
NODE_ENV: "development",
PORT: String(appPort),
LISTEN_HOST: "127.0.0.1",
PUBLIC_BASE_URL: baseUrl,
ALLOWED_ORIGINS: baseUrl,
HOST_ADMISSION_PASSWORD: accessKey,
ROOM_DATABASE_PATH: "",
PEER_ASSISTED_MEDIA: "false",
PEER_ASSISTED_ROOM_IDS: "",
STUN_URLS: "",
});
const roomStore = new ObservedRoomStore({
ttlMs: config.roomTtlMs,
maxRooms: config.maxRooms,
maxViewersPerRoom: config.maxViewersPerRoom,
}, () => senderLedger.record({ hostWssAuthenticated: true }));
server = await createScreenerServer({ config, roomStore });
observeNodeStages(server, senderLedger);
await server.listen(appPort, "127.0.0.1");
native = spawn(goPath, ["run", "./cmd/screener-one-viewer-gate"], {
cwd: join(root, "native", "sender"),
stdio: "pipe",
windowsHide: true,
});
native.stderr.resume();
const launchUrl = await readLaunchUrl(native);
nativePort = Number(new URL(launchUrl).port) || null;
chrome = spawn(chromePath, [
`--remote-debugging-port=${debugPort}`,
`--user-data-dir=${profile}`,
"--no-first-run",
"--no-default-browser-check",
"--disable-extensions",
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows",
"--disable-renderer-backgrounding",
"--autoplay-policy=no-user-gesture-required",
"--auto-select-tab-capture-source-by-title=Native Gate Source",
"--window-size=1280,900",
"about:blank",
], { stdio: "pipe", windowsHide: true });
chrome.stdout.resume();
chrome.stderr.resume();
const version = await waitForVersion(debugPort, chrome);
cdp = await CdpConnection.connect(version.webSocketDebuggerUrl, Date.now() + 5_000);
report.stages.preflight = {
serverInMemory: config.roomDatabasePath === undefined,
peerAssistedDisabled: !config.peerAssistedMedia,
oneViewerOnly: true,
chromeMajor: Number(version.Browser.match(/Chrome\/(\d+)/)?.[1] ?? 0),
};
report.failedStage = "sender-start";
await createPage(cdp, animatedSourceUrl());
const senderPage = await createPage(cdp, launchUrl, senderProbe());
await evaluate<void>(cdp, senderPage, `(() => {
document.querySelector('#server-url').value = ${JSON.stringify(baseUrl)};
document.querySelector('#password').value = ${JSON.stringify(accessKey)};
document.querySelector('#start').click();
})()`, Date.now() + 5_000);
await waitForSenderStart(
async (deadline) => senderStartObservation(await senderSnapshot(cdp!, senderPage, deadline)),
senderLedger,
{ timeoutMs: 20_000 },
);
const senderReady = await senderSnapshot(cdp, senderPage, Date.now() + 3_000);
const senderStartChecks = {
oneEncoderObject: senderReady.encoderInstances === 1,
currentBridge: retainsFirstBridgeSend(senderReady),
noCriticalErrors: senderReady.encoderErrors === 0 && senderReady.fatalEvents === 0,
};
report.stages["sender-start"] = { ...senderLedger.snapshot(), ...senderStartChecks };
requireChecks(senderStartChecks);
report.failedStage = "node-host";
const inviteUrl = await evaluate<string>(cdp, senderPage,
`document.querySelector('#invite-link').href`, Date.now() + 3_000);
const invite = new URL(inviteUrl);
const roomId = invite.pathname.match(/^\/r\/([1-9][0-9]{0,11})$/)?.[1] ?? "";
const viewerGrant = new URLSearchParams(invite.hash.slice(1)).get("v") ?? "";
const nodeHost = {
privateGrantBoundToRoom: new RegExp(`^g1\\.${roomId}\\.[1-9][0-9]{0,12}\\.[A-Za-z0-9_-]{43}$`).test(viewerGrant),
provisionalRandomRoom: roomId !== "1",
};
report.stages["node-host"] = nodeHost;
requireChecks(nodeHost);
report.failedStage = "viewer-signal";
const viewerPage = await createPage(cdp, inviteUrl, viewerProbe());
let identity: OneViewerIdentity | null = null;
await waitForSample(async (deadline) => ({
viewer: await viewerSnapshot(cdp!, viewerPage, deadline),
sender: await senderSnapshot(cdp!, senderPage, deadline),
}), ({ viewer, sender }) => {
identity = captureOneViewerIdentity(viewer.identity, senderIdentity(sender));
const checks = {
viewerAuthenticated: viewer.signaling.authenticateSent === 1 &&
viewer.signaling.authenticatedReceived === 1,
peerJoined: sender.activeViewers === 1 && roomStore.getConnectedViewers(roomId).length === 1,
offerAnswer: viewer.signaling.offersReceived >= 1 && viewer.signaling.answersSent >= 1,
iceExchange: viewer.signaling.candidatesReceived >= 1 && viewer.signaling.candidatesSent >= 1,
connected: viewer.connectionState === "connected" &&
["connected", "completed"].includes(viewer.iceConnectionState),
pionConnected: sender.peers.length === 1 && sender.peers[0]?.connectionState === "connected" &&
["connected", "completed"].includes(sender.peers[0]?.iceConnectionState ?? ""),
sameGeneration: identity !== null,
sameBridge: retainsFirstBridgeSend(sender),
edgeBound: sender.maxPeers <= 1 && sender.peers.length === 1,
};
report.stages["viewer-signal"] = {
...checks,
offersReceived: viewer.signaling.offersReceived,
answersSent: viewer.signaling.answersSent,
candidatesReceived: viewer.signaling.candidatesReceived,
candidatesSent: viewer.signaling.candidatesSent,
socketOrdinal: identity?.socketOrdinal ?? 0,
authGeneration: identity?.authGeneration ?? 0,
connectionOrdinal: identity?.connectionOrdinal ?? 0,
pcOrdinal: identity?.pcOrdinal ?? 0,
trackOrdinal: identity?.trackOrdinal ?? 0,
pionSlot: identity?.pionSlot ?? -1,
pionEdgeGeneration: identity?.pionEdgeGeneration ?? 0,
};
return Object.values(checks).every(Boolean);
}, 20_000);
report.failedStage = "viewer-media";
if (!identity) throw new Error("Viewer generation was not established");
const mediaBaselineDeadline = Date.now() + 3_000;
const [beforeViewer, beforeSender] = await Promise.all([
viewerSnapshot(cdp, viewerPage, mediaBaselineDeadline),
senderSnapshot(cdp, senderPage, mediaBaselineDeadline),
]);
if (!retainsOneViewerIdentity(identity, beforeViewer.identity, senderIdentity(beforeSender))) {
throw new Error("Viewer generation changed before media sampling");
}
await waitForSample(async (deadline) => ({
viewer: await viewerSnapshot(cdp!, viewerPage, deadline),
sender: await senderSnapshot(cdp!, senderPage, deadline),
}), ({ viewer, sender }) => {
const checks = {
inboundPackets: viewer.packetsReceived > beforeViewer.packetsReceived,
inboundBytes: viewer.bytesReceived > beforeViewer.bytesReceived,
decodedFrames: viewer.framesDecoded > beforeViewer.framesDecoded,
videoReady: viewer.videoReadyState >= 2 && viewer.videoWidth > 0 && viewer.videoHeight > 0,
playbackClock: viewer.videoCurrentTime > beforeViewer.videoCurrentTime,
renderedFrames: viewer.renderedFrames > beforeViewer.renderedFrames,
pionOutbound: (sender.peers[0]?.packetsSent ?? 0) > (beforeSender.peers[0]?.packetsSent ?? 0) &&
(sender.peers[0]?.bytesSent ?? 0) > (beforeSender.peers[0]?.bytesSent ?? 0),
sameGeneration: retainsOneViewerIdentity(identity!, viewer.identity, senderIdentity(sender)),
sameBridge: retainsFirstBridgeSend(sender),
oneEncoderObject: sender.encoderInstances === 1,
criticalErrors: sender.fatalEvents === 0 && sender.encoderErrors === 0,
};
report.stages["viewer-media"] = {
...checks,
packetDelta: viewer.packetsReceived - beforeViewer.packetsReceived,
byteDelta: viewer.bytesReceived - beforeViewer.bytesReceived,
decodedDelta: viewer.framesDecoded - beforeViewer.framesDecoded,
renderedDelta: viewer.renderedFrames - beforeViewer.renderedFrames,
pionPacketDelta: (sender.peers[0]?.packetsSent ?? 0) - (beforeSender.peers[0]?.packetsSent ?? 0),
pcOrdinal: viewer.identity.pcOrdinal,
pionSlot: sender.peers[0]?.slot ?? -1,
pionEdgeGeneration: sender.peers[0]?.edgeGeneration ?? 0,
route: safeRoute(sender.peers[0]?.route),
videoWidth: viewer.videoWidth,
videoHeight: viewer.videoHeight,
};
return Object.values(checks).every(Boolean);
}, 10_000);
const finalSender = await senderSnapshot(cdp, senderPage, Date.now() + 3_000);
const finalChecks = { finalBridge: retainsFirstBridgeSend(finalSender) };
report.stages["viewer-media"] = { ...report.stages["viewer-media"], ...finalChecks };
requireChecks(finalChecks);
gateSucceeded = true;
} catch {
// The bounded report retains only the first stage and sanitized counters.
} finally {
report.stages["sender-start"] = {
...report.stages["sender-start"],
...senderLedger.snapshot(),
};
await finalizeGate(report, gateSucceeded, () => cleanupRun({
cdp, native, chrome, server, profile,
ports: [appPort, debugPort, nativePort].filter((port): port is number => port !== null),
}));
console.log(JSON.stringify(report, null, 2));
if (report.status === "failed") process.exitCode = 1;
}
}
export function senderProbe(): string {
return `(() => {
const state = { getDisplayMediaRequested: false, getDisplayMediaResolved: false,
ready: 0, configAccepted: 0,
encoderInstances: 0, encoderOutputs: 0, encoderErrors: 0, fatalEvents: 0,
bridgeGeneration: 0, binarySendAttempts: 0, binarySendSucceeded: 0, binarySendFailed: 0,
activeViewers: 0, maxPeers: 0, edgeGeneration: 0, activeEdgeSlot: null,
config: null, media: null, peers: [] };
const finite = (value) => Number.isFinite(value) ? value : 0;
const mark = (key) => { state[key] = 1; };
let activeBridgeSocket = null;
const nativeGetDisplayMedia = navigator.mediaDevices.getDisplayMedia.bind(navigator.mediaDevices);
Object.defineProperty(navigator.mediaDevices, 'getDisplayMedia', {
configurable: true,
value: (...args) => {
state.getDisplayMediaRequested = true;
return nativeGetDisplayMedia(...args).then((stream) => {
state.getDisplayMediaResolved = true;
return stream;
});
},
});
const NativeWebSocket = globalThis.WebSocket;
globalThis.WebSocket = class GateWebSocket extends NativeWebSocket {
constructor(...args) {
super(...args);
this.__gateMedia = new URL(String(args[0]), location.href).pathname === '/media';
if (!this.__gateMedia) return;
state.bridgeGeneration = Math.min(2, state.bridgeGeneration + 1);
this.__gateBridgeGeneration = state.bridgeGeneration;
activeBridgeSocket = state.bridgeGeneration === 1 ? this : null;
this.addEventListener('message', (event) => {
if (typeof event.data !== 'string') return;
let message; try { message = JSON.parse(event.data); } catch { return; }
if (message.kind === 'ready') state.ready += 1;
if (message.kind === 'config-accepted') state.configAccepted += 1;
if (message.kind === 'fatal') state.fatalEvents += 1;
if (message.kind === 'ready' || message.kind === 'viewer-count') {
state.activeViewers = finite(message.activeViewers);
}
if (message.kind === 'diagnostics') {
state.media = message.media ? {
framesWritten: finite(message.media.framesWritten),
sourceRtpPacketsWritten: finite(message.media.sourceRtpPacketsWritten),
sourceRtpBytesWritten: finite(message.media.sourceRtpBytesWritten),
} : null;
const peers = Array.isArray(message.peers) ? message.peers.slice(0, 3) : [];
const nextSlot = peers.length === 1 ? finite(peers[0].slot) : null;
if (nextSlot !== null && nextSlot !== state.activeEdgeSlot) state.edgeGeneration += 1;
state.activeEdgeSlot = nextSlot;
state.peers = peers.map((peer) => ({
slot: finite(peer.slot), edgeGeneration: state.edgeGeneration,
connectionState: String(peer.connectionState || 'unknown'),
iceConnectionState: String(peer.iceConnectionState || 'unknown'),
route: String(peer.route || 'unknown'),
packetsSent: finite(peer.packetsSent), bytesSent: finite(peer.bytesSent),
}));
state.maxPeers = Math.max(state.maxPeers, state.peers.length);
}
});
}
send(value) {
if (this.__gateMedia && this === activeBridgeSocket &&
this.__gateBridgeGeneration === 1 && value instanceof ArrayBuffer) {
mark('binarySendAttempts');
try {
const result = super.send(value);
mark('binarySendSucceeded');
return result;
} catch (error) {
mark('binarySendFailed');
throw error;
}
}
if (this.__gateMedia) {
if (typeof value === 'string') {
let message; try { message = JSON.parse(value); } catch { message = null; }
if (message?.kind === 'config') state.config = {
codec: typeof message.codec === 'string' ? message.codec : null,
width: finite(message.width), height: finite(message.height), fps: finite(message.fps),
bitrate: finite(message.bitrate), encoderInstances: finite(message.encoderInstances),
};
}
}
return super.send(value);
}
};
if (typeof globalThis.VideoEncoder === 'function') {
const NativeVideoEncoder = globalThis.VideoEncoder;
globalThis.VideoEncoder = new Proxy(NativeVideoEncoder, {
construct(Target, args) {
const init = args[0] || {};
const wrapped = { ...init,
output: (...values) => { state.encoderOutputs += 1; return init.output(...values); },
error: (...values) => { state.encoderErrors += 1; return init.error(...values); },
};
const encoder = Reflect.construct(Target, [wrapped], Target);
state.encoderInstances += 1;
return encoder;
},
get(Target, property, receiver) {
const value = Reflect.get(Target, property, receiver);
return typeof value === 'function' && property === 'isConfigSupported' ? value.bind(Target) : value;
},
});
}
Object.defineProperty(globalThis, '__NATIVE_ONE_VIEWER_GATE__', {
value: { snapshot: () => structuredClone(state) }, configurable: false,
});
})();`;
}
function viewerProbe(): string {
return `(() => {
const state = { signaling: { authenticateSent: 0, authenticatedReceived: 0,
offersReceived: 0, answersSent: 0, candidatesReceived: 0, candidatesSent: 0 },
socketCount: 0, authenticateSocketOrdinal: 0, authSocketOrdinal: 0, authGeneration: 0,
connectionIds: [], activeConnectionId: null, consistent: true, pendingOffer: null,
pcs: [], boundPcOrdinal: 0, boundConnectionOrdinal: 0, boundSocketOrdinal: 0,
boundAuthGeneration: 0, trackCount: 0, trackOrdinal: 0, trackPcOrdinal: 0,
renderedFrames: 0, observedVideo: null };
const connectionOrdinal = (value) => {
let index = state.connectionIds.indexOf(value);
if (index < 0) { state.connectionIds.push(value); index = state.connectionIds.length - 1; }
return index + 1;
};
const inspect = (data, direction, socketOrdinal) => {
if (typeof data !== 'string') return;
let message; try { message = JSON.parse(data); } catch { return; }
if (direction === 'out' && message.type === 'authenticate') {
state.signaling.authenticateSent += 1; state.authenticateSocketOrdinal = socketOrdinal; return;
}
if (direction === 'in' && message.type === 'authenticated') {
state.signaling.authenticatedReceived += 1; state.authGeneration += 1;
state.authSocketOrdinal = socketOrdinal;
if (state.authenticateSocketOrdinal !== socketOrdinal) state.consistent = false;
return;
}
if (message.type !== 'signal' || !message.payload) return;
const rawConnectionId = message.payload.connectionId;
if (typeof rawConnectionId !== 'string' || state.authSocketOrdinal !== socketOrdinal) {
state.consistent = false; return;
}
const ordinal = connectionOrdinal(rawConnectionId);
if (state.activeConnectionId === null && direction === 'in' &&
message.payload.kind === 'description' && message.payload.description?.type === 'offer') {
state.activeConnectionId = rawConnectionId;
}
if (state.activeConnectionId !== rawConnectionId) state.consistent = false;
if (direction === 'in' && message.payload.kind === 'description' && message.payload.description?.type === 'offer') {
state.signaling.offersReceived += 1;
state.pendingOffer = { rawConnectionId, ordinal, socketOrdinal, authGeneration: state.authGeneration };
}
if (direction === 'out' && message.payload.kind === 'description' && message.payload.description?.type === 'answer') {
state.signaling.answersSent += 1;
if (state.boundConnectionOrdinal !== ordinal || state.boundSocketOrdinal !== socketOrdinal ||
state.boundAuthGeneration !== state.authGeneration) state.consistent = false;
}
if (direction === 'in' && message.payload.kind === 'candidate') state.signaling.candidatesReceived += 1;
if (direction === 'out' && message.payload.kind === 'candidate') state.signaling.candidatesSent += 1;
};
const NativeWebSocket = globalThis.WebSocket;
globalThis.WebSocket = class GateWebSocket extends NativeWebSocket {
constructor(...args) {
super(...args);
this.__gateSignal = new URL(String(args[0]), location.href).pathname === '/signal';
if (this.__gateSignal) {
this.__gateSocketOrdinal = ++state.socketCount;
this.addEventListener('message', (event) => inspect(event.data, 'in', this.__gateSocketOrdinal));
}
}
send(value) {
if (this.__gateSignal) inspect(value, 'out', this.__gateSocketOrdinal);
return super.send(value);
}
};
const NativePeerConnection = globalThis.RTCPeerConnection;
globalThis.RTCPeerConnection = class GatePeerConnection extends NativePeerConnection {
constructor(...args) {
super(...args);
state.pcs.push(this);
this.__gatePcOrdinal = state.pcs.length;
this.addEventListener('track', (event) => {
if (event.track?.kind !== 'video') return;
state.trackCount += 1; state.trackOrdinal = state.trackCount;
state.trackPcOrdinal = this.__gatePcOrdinal;
});
}
async setRemoteDescription(description) {
const result = await super.setRemoteDescription(description);
if (description?.type === 'offer') {
const offer = state.pendingOffer;
if (!offer || offer.rawConnectionId !== state.activeConnectionId) state.consistent = false;
else {
state.boundPcOrdinal = this.__gatePcOrdinal;
state.boundConnectionOrdinal = offer.ordinal;
state.boundSocketOrdinal = offer.socketOrdinal;
state.boundAuthGeneration = offer.authGeneration;
}
}
return result;
}
};
async function snapshot() {
const video = document.querySelector('.remote-stage video');
if (video && state.observedVideo !== video && typeof video.requestVideoFrameCallback === 'function') {
state.observedVideo = video;
const frame = () => { state.renderedFrames += 1; video.requestVideoFrameCallback(frame); };
video.requestVideoFrameCallback(frame);
}
const pc = state.pcs[state.boundPcOrdinal - 1];
let packetsReceived = 0, bytesReceived = 0, framesDecoded = 0;
if (pc) {
const stats = await pc.getStats();
stats.forEach((entry) => {
if (entry.type === 'inbound-rtp' && entry.kind === 'video' && entry.isRemote !== true) {
packetsReceived += finite(entry.packetsReceived);
bytesReceived += finite(entry.bytesReceived);
framesDecoded += finite(entry.framesDecoded);
}
});
}
const activeOrdinal = state.activeConnectionId === null ? 0 :
state.connectionIds.indexOf(state.activeConnectionId) + 1;
const identity = { socketCount: state.socketCount, socketOrdinal: state.authSocketOrdinal,
authGeneration: state.authGeneration, connectionCount: state.connectionIds.length,
connectionOrdinal: activeOrdinal, pcCount: state.pcs.length, pcOrdinal: state.boundPcOrdinal,
trackCount: state.trackCount, trackOrdinal: state.trackOrdinal,
consistent: state.consistent && state.boundConnectionOrdinal === activeOrdinal &&
state.boundSocketOrdinal === state.authSocketOrdinal &&
state.boundAuthGeneration === state.authGeneration && state.trackPcOrdinal === state.boundPcOrdinal };
return { signaling: structuredClone(state.signaling), identity,
connectionState: pc?.connectionState || 'none',
iceConnectionState: pc?.iceConnectionState || 'none', packetsReceived, bytesReceived, framesDecoded,
videoReadyState: finite(video?.readyState), videoWidth: finite(video?.videoWidth),
videoHeight: finite(video?.videoHeight), videoCurrentTime: finite(video?.currentTime),
renderedFrames: state.renderedFrames };
}
const finite = (value) => Number.isFinite(value) ? value : 0;
Object.defineProperty(globalThis, '__NATIVE_VIEWER_GATE__', {
value: { snapshot }, configurable: false,
});
})();`;
}
async function createPage(cdp: CdpConnection, url: string, probe?: string): Promise<PageHandle> {
const deadline = Date.now() + 15_000;
const created = await cdp.call<{ targetId: string }>("Target.createTarget", { url: "about:blank", background: false }, undefined, deadline);
const attached = await cdp.call<{ sessionId: string }>("Target.attachToTarget", { targetId: created.targetId, flatten: true }, undefined, deadline);
const page = { sessionId: attached.sessionId };
await Promise.all([
cdp.call("Page.enable", {}, page.sessionId, deadline),
cdp.call("Runtime.enable", {}, page.sessionId, deadline),
]);
if (probe) await cdp.call("Page.addScriptToEvaluateOnNewDocument", { source: probe }, page.sessionId, deadline);
await cdp.call("Page.navigate", { url }, page.sessionId, deadline);
await waitForSample(
(sampleDeadline) => evaluate(cdp, page, "document.readyState === 'complete'", sampleDeadline),
Boolean,
Math.max(1, deadline - Date.now()),
);
return page;
}
async function evaluate<T>(cdp: CdpConnection, page: PageHandle, expression: string, deadline: number): Promise<T> {
const value = await cdp.call<CdpResult<T>>("Runtime.evaluate", {
expression, awaitPromise: true, returnByValue: true, userGesture: true,
}, page.sessionId, deadline);
if (value.exceptionDetails) throw new Error("Browser evaluation failed");
return value.result.value as T;
}
function senderSnapshot(cdp: CdpConnection, page: PageHandle, deadline: number): Promise<SenderSnapshot> {
return evaluate(cdp, page, "globalThis.__NATIVE_ONE_VIEWER_GATE__.snapshot()", deadline);
}
function viewerSnapshot(cdp: CdpConnection, page: PageHandle, deadline: number): Promise<ViewerSnapshot> {
return evaluate(cdp, page, "globalThis.__NATIVE_VIEWER_GATE__.snapshot()", deadline);
}
function senderIdentity(sender: SenderSnapshot): SenderIdentityEvidence {
const peer = sender.peers[0];
return { peerCount: sender.peers.length, maxPeers: sender.maxPeers,
slot: peer?.slot ?? -1, edgeGeneration: peer?.edgeGeneration ?? 0 };
}
function requireChecks(checks: Record<string, boolean>): void {
if (!Object.values(checks).every(Boolean)) throw new Error("Gate check failed");
}
function safeRoute(value: string | undefined): string {
return new Set(["direct-udp", "direct-tcp", "turn-udp", "turn-tcp", "turn-tls", "turn-unknown"])
.has(value ?? "") ? value! : "unknown";
}
async function reservePort(): Promise<number> {
const server = createServer();
return new Promise((resolvePort, rejectPort) => {
server.once("error", rejectPort);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (!address || typeof address === "string") return rejectPort(new Error("Port reservation failed"));
server.close((error) => error ? rejectPort(error) : resolvePort(address.port));
});
});
}
async function readLaunchUrl(child: ChildProcessWithoutNullStreams): Promise<string> {
let buffer = "";
return new Promise((resolveUrl, rejectUrl) => {
const timeout = setTimeout(() => rejectUrl(new Error("Native runner startup timed out")), 20_000);
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const end = buffer.indexOf("\n");
if (end < 0) return;
clearTimeout(timeout);
try {
const parsed = JSON.parse(buffer.slice(0, end)) as { launchUrl?: unknown };
if (typeof parsed.launchUrl !== "string") throw new Error();
resolveUrl(parsed.launchUrl);
} catch {
rejectUrl(new Error("Native runner handoff was invalid"));
}
});
child.once("exit", () => {
clearTimeout(timeout);
rejectUrl(new Error("Native runner exited before handoff"));
});
});
}
async function waitForVersion(port: number, child: ChildProcessWithoutNullStreams): Promise<{ Browser: string; webSocketDebuggerUrl: string }> {
const deadline = Date.now() + 20_000;
while (Date.now() < deadline && child.exitCode === null) {
try {
return await fetchJsonBefore<{ Browser: string; webSocketDebuggerUrl: string }>(
`http://127.0.0.1:${port}/json/version`, deadline,
);
} catch {}
await delay(Math.min(100, Math.max(0, deadline - Date.now())));
}
throw new Error("Chrome startup timed out");
}
interface CleanupResources {
cdp: CdpConnection | null;
native: ChildProcessWithoutNullStreams | null;
chrome: ChildProcessWithoutNullStreams | null;
server: ScreenerServer | null;
profile: string | null;
ports: number[];
}
async function cleanupRun(resources: CleanupResources): Promise<CleanupResult> {
if (resources.cdp) {
try { await resources.cdp.call("Browser.close", {}, undefined, Date.now() + 2_000); } catch {}
resources.cdp.close();
}
const browserExited = resources.chrome ? await stopProcessTree(resources.chrome) : true;
if (resources.native && processRunning(resources.native)) {
try { resources.native.stdin.write("\n"); } catch {}
}
const nativeExited = resources.native ? await stopProcessTree(resources.native) : true;
let serverClosed = true;
if (resources.server) {
try { await withDeadline(() => resources.server!.close(), Date.now() + 5_000); } catch { serverClosed = false; }
}
const portsClosed = await waitForPortsClosed([...new Set(resources.ports)], 5_000);
const profileRemoved = resources.profile ? await removeGateProfile(resources.profile) : true;
return { browserExited, nativeExited, serverClosed, portsClosed, profileRemoved };
}
function processRunning(child: ChildProcessWithoutNullStreams): boolean {
return child.exitCode === null && child.signalCode === null;
}
async function stopProcessTree(child: ChildProcessWithoutNullStreams): Promise<boolean> {
if (!processRunning(child)) return true;
if (await waitForExit(child, 2_000)) return true;
if (process.platform !== "win32" || child.pid === undefined) return false;
await runBounded("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], 5_000);
return waitForExit(child, 3_000);
}
async function waitForExit(child: ChildProcessWithoutNullStreams, timeoutMs: number): Promise<boolean> {
if (!processRunning(child)) return true;
return new Promise<boolean>((resolveExit) => {
const onExit = () => { clearTimeout(timeout); resolveExit(true); };
const timeout = setTimeout(() => {
child.off("exit", onExit);
resolveExit(!processRunning(child));
}, timeoutMs);
child.once("exit", onExit);
});
}
async function runBounded(command: string, args: string[], timeoutMs: number,
environment: NodeJS.ProcessEnv = process.env): Promise<boolean> {
const child = spawn(command, args, { stdio: "pipe", windowsHide: true, env: environment });
child.stdout.resume();
child.stderr.resume();
try {
const code = await withDeadline(() => new Promise<number | null>((resolveExit, rejectExit) => {
child.once("error", rejectExit);
child.once("exit", resolveExit);
}), Date.now() + timeoutMs);
return code === 0;
} catch {
child.kill();
return false;
}
}
async function waitForPortsClosed(ports: number[], timeoutMs: number): Promise<boolean> {
if (ports.length === 0) return true;
try {
await waitForSample(
(deadline) => Promise.all(ports.map((port) => portClosed(port, deadline))),
(closed) => closed.every(Boolean),
timeoutMs,
);
return true;
} catch {
return false;
}
}
function portClosed(port: number, deadline: number): Promise<boolean> {
return withDeadline(() => new Promise<boolean>((resolveClosed) => {
const socket = createConnection({ host: "127.0.0.1", port });
const finish = (closed: boolean) => { socket.destroy(); resolveClosed(closed); };
socket.once("connect", () => finish(false));
socket.once("error", () => finish(true));
}), deadline);
}
export const profileCleanupScript = String.raw`
$ErrorActionPreference = 'Stop'
$root = [IO.Path]::GetFullPath($env:SCREENER_GATE_PROFILE_TO_REMOVE)
$temp = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd([IO.Path]::DirectorySeparatorChar)
$parent = [IO.Path]::GetDirectoryName($root).TrimEnd([IO.Path]::DirectorySeparatorChar)
$name = [IO.Path]::GetFileName($root)
if ($parent -ine $temp -or $name -notmatch '^screener-native-one-viewer-[A-Za-z0-9_-]{6}$') { exit 31 }
$rootEntries = @([IO.Directory]::EnumerateFileSystemEntries($parent, $name, [IO.SearchOption]::TopDirectoryOnly))
if ($rootEntries.Count -eq 0) { exit 0 }
if ($rootEntries.Count -ne 1) { exit 34 }
$root = $rootEntries[0]
$rootAttributes = [IO.File]::GetAttributes($root)
if (($rootAttributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { exit 32 }
if (($rootAttributes -band [IO.FileAttributes]::Directory) -eq 0) { exit 34 }
$pending = [Collections.Generic.Stack[string]]::new()
$pending.Push($root)
while ($pending.Count -gt 0) {
$directory = $pending.Pop()
$attributes = [IO.File]::GetAttributes($directory)
if (($attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { exit 32 }
foreach ($entry in [IO.Directory]::EnumerateFileSystemEntries($directory)) {
$attributes = [IO.File]::GetAttributes($entry)
if (($attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { exit 32 }
if (($attributes -band [IO.FileAttributes]::Directory) -ne 0) { $pending.Push($entry) }
}
}
Remove-Item -LiteralPath $root -Recurse -Force
$rootEntries = @([IO.Directory]::EnumerateFileSystemEntries($parent, $name, [IO.SearchOption]::TopDirectoryOnly))
if ($rootEntries.Count -ne 0) { exit 33 }
`;
async function removeGateProfile(profile: string): Promise<boolean> {
if (process.platform !== "win32" || !isExactGateProfile(profile, tmpdir())) return false;
return runBounded("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", profileCleanupScript],
10_000, { ...process.env, SCREENER_GATE_PROFILE_TO_REMOVE: profile });
}
function animatedSourceUrl(): string {
const html = `<!doctype html><title>Native Gate Source</title><canvas width="1280" height="720"></canvas>
<script>const c=document.querySelector('canvas'),x=c.getContext('2d');let f=0;
setInterval(()=>{f++;x.fillStyle='hsl('+f%360+' 80% 35%)';x.fillRect(0,0,1280,720);
x.fillStyle='white';x.fillRect(f*13%1280,0,20,720);x.font='64px monospace';x.fillText(f,40,90)},33);<\/script>`;
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
}
function delay(ms: number): Promise<void> {
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
await main();
}
+157
View File
@@ -0,0 +1,157 @@
export interface SenderStartObservation {
getDisplayMediaRequested: boolean;
getDisplayMediaResolved: boolean;
hostAdmissionAccepted: boolean;
roomCreated: boolean;
hostWssAuthenticated: boolean;
bridgeConnected: boolean;
fixedHighConfigured: boolean;
firstEncodedChunk: boolean;
bridgeGeneration: number;
binarySendAttempts: number;
binarySendSucceeded: number;
binarySendFailed: number;
encoderInstances: number;
framesWritten: number;
sourceRtpPacketsWritten: number;
sourceRtpBytesWritten: number;
}
export interface SenderStartLedgerRecord extends SenderStartObservation {
schemaVersion: 1;
sequence: number;
}
type LedgerSink = (record: Readonly<SenderStartLedgerRecord>) => void;
const initialObservation: SenderStartObservation = {
getDisplayMediaRequested: false,
getDisplayMediaResolved: false,
hostAdmissionAccepted: false,
roomCreated: false,
hostWssAuthenticated: false,
bridgeConnected: false,
fixedHighConfigured: false,
firstEncodedChunk: false,
bridgeGeneration: 0,
binarySendAttempts: 0,
binarySendSucceeded: 0,
binarySendFailed: 0,
encoderInstances: 0,
framesWritten: 0,
sourceRtpPacketsWritten: 0,
sourceRtpBytesWritten: 0,
};
const booleanKeys = [
"getDisplayMediaRequested",
"getDisplayMediaResolved",
"hostAdmissionAccepted",
"roomCreated",
"hostWssAuthenticated",
"bridgeConnected",
"fixedHighConfigured",
"firstEncodedChunk",
] as const;
const counterKeys = [
"binarySendAttempts",
"binarySendSucceeded",
"binarySendFailed",
"encoderInstances",
"framesWritten",
"sourceRtpPacketsWritten",
"sourceRtpBytesWritten",
] as const;
// The first positive value is enough for this stage and bounds the append-only
// ledger to at most one record per field transition.
const counterLimit = 1_000_000_000;
export class SenderStartLedger {
private observation = { ...initialObservation };
private sequence = 0;
constructor(private readonly sink: LedgerSink = () => undefined) {}
record(patch: Partial<SenderStartObservation>): void {
const next = { ...this.observation };
for (const key of booleanKeys) {
if (patch[key] === true) next[key] = true;
}
for (const key of counterKeys) {
const value = patch[key];
if (next[key] === 0 && typeof value === "number" && Number.isFinite(value) && value > 0) {
next[key] = Math.min(Math.floor(value), counterLimit);
}
}
const bridgeGeneration = patch.bridgeGeneration;
if (typeof bridgeGeneration === "number" && Number.isFinite(bridgeGeneration) &&
bridgeGeneration > next.bridgeGeneration) {
next.bridgeGeneration = Math.min(Math.floor(bridgeGeneration), 2);
}
if (JSON.stringify(next) === JSON.stringify(this.observation)) return;
this.observation = next;
this.sequence += 1;
this.sink(Object.freeze(this.snapshot()));
}
snapshot(): SenderStartLedgerRecord {
return {
schemaVersion: 1,
sequence: this.sequence,
...this.observation,
};
}
}
export function senderStartComplete(record: SenderStartObservation): boolean {
return booleanKeys.every((key) => record[key]) && record.encoderInstances === 1 &&
record.bridgeGeneration === 1 && record.binarySendAttempts > 0 &&
record.binarySendSucceeded > 0 && record.binarySendFailed === 0 &&
record.framesWritten > 0 && record.sourceRtpPacketsWritten > 0;
}
interface WaitOptions {
timeoutMs: number;
pollMs?: number;
now?: () => number;
sleep?: (milliseconds: number) => Promise<void>;
}
export async function waitForSenderStart(
sample: (deadline: number) => Promise<Partial<SenderStartObservation>>,
ledger: SenderStartLedger,
options: WaitOptions,
): Promise<SenderStartLedgerRecord> {
const now = options.now ?? Date.now;
const sleep = options.sleep ?? delay;
const deadline = now() + options.timeoutMs;
for (;;) {
let observation: Partial<SenderStartObservation> | undefined;
try {
observation = await settleBefore(sample(deadline), Math.max(0, deadline - now()));
} catch {
// A failed sample cannot erase the last synchronously committed stage.
}
if (observation) ledger.record(observation);
const current = ledger.snapshot();
if (senderStartComplete(current)) return current;
if (now() >= deadline) throw new Error("Sender-start stage timed out");
await sleep(options.pollMs ?? 100);
}
}
function settleBefore<T>(pending: Promise<T>, milliseconds: number): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error("Sender-start sample timed out")), milliseconds);
pending.then(
(value) => { clearTimeout(timeout); resolve(value); },
(error: unknown) => { clearTimeout(timeout); reject(error); },
);
});
}
function delay(milliseconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
+195
View File
@@ -0,0 +1,195 @@
import { join } from "node:path";
import { runInNewContext } from "node:vm";
import { describe, expect, it, vi } from "vitest";
import {
captureOneViewerIdentity,
fetchJsonBefore,
finalizeGate,
isExactGateProfile,
retainsFirstBridgeSend,
retainsOneViewerIdentity,
waitForSample,
type CleanupResult,
type SenderIdentityEvidence,
type ViewerIdentityEvidence,
} from "../scripts/native-one-viewer-gate-core";
import { profileCleanupScript, senderProbe } from "../scripts/native-one-viewer-gate";
const viewer: ViewerIdentityEvidence = {
socketCount: 1, socketOrdinal: 1, authGeneration: 1,
connectionCount: 1, connectionOrdinal: 1, pcCount: 1, pcOrdinal: 1,
trackCount: 1, trackOrdinal: 1, consistent: true,
};
const sender: SenderIdentityEvidence = {
peerCount: 1, maxPeers: 1, slot: 1, edgeGeneration: 1,
};
const clean: CleanupResult = {
browserExited: true, nativeExited: true, serverClosed: true,
portsClosed: true, profileRemoved: true,
};
interface BridgeProbeSnapshot {
bridgeGeneration: number;
binarySendAttempts: number;
binarySendSucceeded: number;
binarySendFailed: number;
}
class ProbeWebSocket {
failBinary = false;
constructor(..._args: unknown[]) {}
addEventListener(..._args: unknown[]): void {}
send(value: unknown): void {
if (this.failBinary && value instanceof ArrayBuffer) throw new Error("not retained");
}
}
function bridgeProbeContext(): {
WebSocket: typeof ProbeWebSocket;
snapshot: () => BridgeProbeSnapshot;
} {
const context: Record<string, unknown> = {
ArrayBuffer,
URL,
structuredClone,
location: { href: "http://127.0.0.1/" },
navigator: { mediaDevices: { getDisplayMedia: async () => ({}) } },
WebSocket: ProbeWebSocket,
};
runInNewContext(senderProbe(), context);
return {
WebSocket: context.WebSocket as typeof ProbeWebSocket,
snapshot: (context.__NATIVE_ONE_VIEWER_GATE__ as { snapshot(): BridgeProbeSnapshot }).snapshot,
};
}
describe("native one-viewer gate invariants", () => {
it("rejects cumulative evidence from a rebuilt Viewer PC", () => {
const baseline = captureOneViewerIdentity(viewer, sender);
expect(baseline).not.toBeNull();
expect(retainsOneViewerIdentity(baseline!, {
...viewer, connectionCount: 2, connectionOrdinal: 2, pcCount: 2, pcOrdinal: 2,
}, sender)).toBe(false);
});
it("rejects a missing Pion slot", () => {
expect(captureOneViewerIdentity(viewer, { ...sender, slot: 0 })).toBeNull();
});
it("records only the current local bridge binary-send outcome", () => {
const successful = bridgeProbeContext();
const first = new successful.WebSocket("ws://127.0.0.1/media");
first.send("config");
first.send(new ArrayBuffer(1));
expect(successful.snapshot()).toMatchObject({
bridgeGeneration: 1,
binarySendAttempts: 1,
binarySendSucceeded: 1,
binarySendFailed: 0,
});
const replacement = new successful.WebSocket("ws://127.0.0.1/media");
replacement.send(new ArrayBuffer(1));
expect(successful.snapshot()).toMatchObject({
bridgeGeneration: 2,
binarySendAttempts: 1,
binarySendSucceeded: 1,
binarySendFailed: 0,
});
const failed = bridgeProbeContext();
const throwing = new failed.WebSocket("ws://127.0.0.1/media");
throwing.failBinary = true;
expect(() => throwing.send(new ArrayBuffer(1))).toThrow("not retained");
expect(failed.snapshot()).toMatchObject({
bridgeGeneration: 1,
binarySendAttempts: 1,
binarySendSucceeded: 0,
binarySendFailed: 1,
});
});
it("rejects a late bridge replacement at every post-start gate", () => {
const senderStart = {
bridgeGeneration: 1,
binarySendAttempts: 1,
binarySendSucceeded: 1,
binarySendFailed: 0,
};
expect(retainsFirstBridgeSend(senderStart)).toBe(true);
const replaced = { ...senderStart, bridgeGeneration: 2 };
expect({
viewerSignal: retainsFirstBridgeSend(replaced),
viewerMedia: retainsFirstBridgeSend(replaced),
finalSuccess: retainsFirstBridgeSend(replaced),
}).toEqual({ viewerSignal: false, viewerMedia: false, finalSuccess: false });
});
it("bounds Chrome version headers and body with one abort deadline", async () => {
vi.useFakeTimers();
try {
const signals: AbortSignal[] = [];
const headers = fetchJsonBefore("http://gate.invalid", Date.now() + 25,
async (_url, init) => {
signals.push(init.signal);
return new Promise<never>(() => undefined);
});
const body = fetchJsonBefore("http://gate.invalid", Date.now() + 25,
async (_url, init) => {
signals.push(init.signal);
return { ok: true, json: () => new Promise<never>(() => undefined) };
});
const headerAssertion = expect(headers).rejects.toThrow("Operation deadline exceeded");
const bodyAssertion = expect(body).rejects.toThrow("Operation deadline exceeded");
await vi.advanceTimersByTimeAsync(30);
await Promise.all([headerAssertion, bodyAssertion]);
expect(signals).toHaveLength(2);
expect(signals.every((signal) => signal.aborted)).toBe(true);
} finally {
vi.useRealTimers();
}
});
it("bounds a hung browser sample by the stage deadline", async () => {
vi.useFakeTimers();
try {
const pending = waitForSample(() => new Promise<never>(() => undefined), () => true, 25, 5);
const assertion = expect(pending).rejects.toThrow("Bounded browser stage timed out");
await vi.advanceTimersByTimeAsync(30);
await assertion;
} finally {
vi.useRealTimers();
}
});
it("cannot pass before cleanup and fails when profile removal fails", async () => {
const report = { status: "failed" as const, failedStage: "viewer-media" as string | null,
stages: {} };
let release!: (result: CleanupResult) => void;
const cleanup = new Promise<CleanupResult>((resolve) => { release = resolve; });
const finishing = finalizeGate(report, true, () => cleanup);
await Promise.resolve();
expect(report.status).toBe("failed");
release(clean);
await finishing;
expect(report).toMatchObject({ status: "passed", failedStage: null });
const failed = { status: "failed" as const, failedStage: null as string | null, stages: {} };
await finalizeGate(failed, true, async () => ({ ...clean, profileRemoved: false }));
expect(failed).toMatchObject({ status: "failed", failedStage: "cleanup" });
});
it("accepts only the exact task profile directly under system Temp", () => {
const temp = join("C:", "Temp");
expect(isExactGateProfile(join(temp, "screener-native-one-viewer-a1B_2-"), temp)).toBe(true);
expect(isExactGateProfile(join(temp, "nested", "screener-native-one-viewer-a1B_2-"), temp)).toBe(false);
expect(isExactGateProfile(join(temp, "other-a1B_2-"), temp)).toBe(false);
expect(profileCleanupScript).toContain("EnumerateFileSystemEntries($parent, $name");
expect(profileCleanupScript).toContain("GetAttributes($root)");
expect(profileCleanupScript).not.toMatch(/\[IO\.(?:File|Directory)\]::Exists/);
});
});
+117
View File
@@ -0,0 +1,117 @@
import { describe, expect, it, vi } from "vitest";
import {
SenderStartLedger,
senderStartComplete,
waitForSenderStart,
type SenderStartObservation,
} from "../scripts/native-one-viewer-ledger";
describe("native one-viewer sender-start ledger", () => {
it("retains the last sanitized stage when an intermediate wait times out", async () => {
const committed: ReturnType<SenderStartLedger["snapshot"]>[] = [];
const ledger = new SenderStartLedger((record) => committed.push({ ...record }));
const samples: Array<Partial<SenderStartObservation>> = [
{ getDisplayMediaRequested: true },
{
getDisplayMediaResolved: true,
hostAdmissionAccepted: true,
roomCreated: true,
},
{
hostWssAuthenticated: true,
bridgeConnected: true,
fixedHighConfigured: true,
firstEncodedChunk: true,
bridgeGeneration: 1,
binarySendAttempts: 1,
binarySendSucceeded: 1,
encoderInstances: 1,
framesWritten: 4,
},
];
let elapsed = 0;
await expect(
waitForSenderStart(
async () => samples.shift() ?? {},
ledger,
{
timeoutMs: 10,
pollMs: 5,
now: () => elapsed,
sleep: async (milliseconds) => { elapsed += milliseconds; },
},
),
).rejects.toThrow("Sender-start stage timed out");
expect(committed.at(-1)).toEqual({
schemaVersion: 1,
sequence: 3,
getDisplayMediaRequested: true,
getDisplayMediaResolved: true,
hostAdmissionAccepted: true,
roomCreated: true,
hostWssAuthenticated: true,
bridgeConnected: true,
fixedHighConfigured: true,
firstEncodedChunk: true,
bridgeGeneration: 1,
binarySendAttempts: 1,
binarySendSucceeded: 1,
binarySendFailed: 0,
encoderInstances: 1,
framesWritten: 4,
sourceRtpPacketsWritten: 0,
sourceRtpBytesWritten: 0,
});
expect(JSON.stringify(committed)).not.toMatch(/token|url|ip|sdp|error/i);
});
it("fails closed and bounds evidence when the bridge generation changes", () => {
const ledger = new SenderStartLedger();
ledger.record({
getDisplayMediaRequested: true,
getDisplayMediaResolved: true,
hostAdmissionAccepted: true,
roomCreated: true,
hostWssAuthenticated: true,
bridgeConnected: true,
fixedHighConfigured: true,
firstEncodedChunk: true,
bridgeGeneration: 1,
binarySendAttempts: Number.MAX_SAFE_INTEGER,
binarySendSucceeded: Number.MAX_SAFE_INTEGER,
encoderInstances: 1,
framesWritten: 1,
sourceRtpPacketsWritten: 1,
});
ledger.record({ bridgeGeneration: 99, binarySendFailed: 1 });
const snapshot = ledger.snapshot();
expect(snapshot).toMatchObject({
bridgeGeneration: 2,
binarySendAttempts: 1_000_000_000,
binarySendSucceeded: 1_000_000_000,
binarySendFailed: 1,
});
expect(senderStartComplete({ ...snapshot, binarySendFailed: 0 })).toBe(false);
expect(senderStartComplete({ ...snapshot, bridgeGeneration: 1 })).toBe(false);
});
it("bounds a sender snapshot that never settles", async () => {
vi.useFakeTimers();
try {
const pending = waitForSenderStart(
() => new Promise<never>(() => undefined),
new SenderStartLedger(),
{ timeoutMs: 25 },
);
const assertion = expect(pending).rejects.toThrow("Sender-start stage timed out");
await vi.advanceTimersByTimeAsync(30);
await assertion;
} finally {
vi.useRealTimers();
}
});
});
+7 -1
View File
@@ -15,5 +15,11 @@
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": ["vite.config.ts", "vitest.config.ts", "tests"]
"include": [
"vite.config.ts",
"vitest.config.ts",
"scripts/native-one-viewer-gate.ts",
"scripts/native-one-viewer-ledger.ts",
"tests"
]
}