# OSHUN V10 Rail web

The Next.js app in this directory is the shared Rail frontend for the standalone
browser fallback and the Tauri webview. It exports a static `out/` bundle so the
native shell can package the same files that the browser surface serves.

Local commands:

- `pnpm dev` — run on `http://127.0.0.1:3095`
- `pnpm test` — component and design-foundation tests
- `pnpm e2e` — desktop and mobile Chromium checks with accessibility automation
- `pnpm e2e:pwa` — production-export installability, offline, and browser-share
  checks
- `pnpm build` — produce the static web bundle in `out/`

## Installable browser fallback

The production export is an installable PWA that keeps the same narrow Rail
workspace and discretion controls in a dedicated browser window. `pnpm build`
generates a revisioned service worker from the completed `out/` tree and fails
if required manifest or icon assets are absent or the shell cache exceeds 32
MiB. The worker caches only same-origin static export assets. Navigation is
network-first with the cached root as its offline fallback; BFF/API responses
are never cached, so offline account policy remains visibly unavailable rather
than being invented or replayed.

The Startup settings ledger separates browser and desktop powers. The installed
web surface provides the offline shell, but system tray, always-on-top, and
system-wide screen-share detection remain desktop-shell capabilities. Browsers
with `getDisplayMedia` expose an explicit **Share from this Rail** action under
Privacy. Only a share started by that action is observed; ending it keeps
personal tiles protected until the user explicitly restores them. Escape still
hides the Rail immediately in-tab.

The existing Clipper extension has an action popup and options page but no
Chrome `side_panel` implementation to reuse. The PWA is therefore the supported
browser-panel fallback; this app does not create a parallel extension surface.
The install button appears only after a real `beforeinstallprompt` event. When a
browser owns installation through its menu, settings say so instead of
simulating a prompt.

To exercise the exact generated manifest, service worker, cache, offline shell,
discretion path, and browser self-share contract on desktop and mobile Chromium:

```bash
pnpm build
pnpm e2e:pwa
```

The RA.9 browser gate drives every registered pilot/test channel at effective
loudness 0, 1, and 2. Initial workspace, section, and micro-act presentation is
static at those default quiet postures; playback remains paused and muted until
an explicit user control invokes it. The focused desktop/mobile command is:

```bash
pnpm exec playwright test e2e/phase-a-invariants.spec.ts \
  --project=desktop-chromium --project=mobile-chromium --workers=1
```

## Veritas Live production channel

Veritas Live is a production directory entry with one public spectator face. Its
cold-cache tile leads with the current tracked event and a compact, motionless
evidence strip for verified, unverified, contradicted, and pending claims. The
adapter re-derives those counts from provenance-safe claim state; the same exact
event and totals remain readable in text-only mode. The tile contains no image,
video, audio, or autoplay surface and links to the specific source event.

The morning Veritas briefing is offered separately through the shared audio lane
from a host-bound published daily edition. It remains subject to the Thread
daypart decision and ordinary user playback authority: acquisition is paused,
and neither the tile nor the channel can start it automatically.

## V1 account-state bridge

The standalone browser and Tauri bundles read Rail user state from the Oshun BFF
(`NEXT_PUBLIC_OSHUN_BFF_URL`, default `http://localhost:4010`). The host
supplies the current V1 bearer at runtime before React mounts:

```ts
window.__OSHUN_RAIL_AUTH__ = {
  getAccessToken: () => currentV1AccessToken,
};
```

No credential is built into the static export. Without the bridge, the Rail
fails visibly with a retryable V1-session notice and keeps account settings
closed. Reads are `no-store`; setting changes render optimistically, reconcile
from the BFF response, and roll back with the server error exposed.

## Ori Presence beta

Ori Presence is a default-off, cohort-gated BFF feature; the static web bundle
contains no client-side override. When eligible, its quiet Thread, recap, voice,
and remembrance surfaces still require their exact user grants. The browser
sends only the already-batched Rail timeline. The BFF removes adult-ring,
unsubscribed, payload, and ungranted awareness data before composing either
provider prompt. Voice is fetched only after an explicit play action and uses a
bounded overlay duck rather than acquiring the audio holder.

An active crisis frame, discretion/screen-share protection, budget exhaustion,
missing consent, missing generation configuration, or a closed cohort keeps the
surface silent. The operator configuration, rollout checks, cost queries, and
rollback procedure are in
[`V10/RB2_ORI_PRESENCE_BETA_RUNBOOK.md`](../../../V10/RB2_ORI_PRESENCE_BETA_RUNBOOK.md).
Focused desktop/mobile coverage is:

```bash
pnpm exec playwright test e2e/ori-presence.spec.ts \
  --project=desktop-chromium --project=mobile-chromium --workers=1
```

## Native tray bridge

Inside Tauri, the shared frontend publishes a bounded tray snapshot through the
official Tauri API. It contains up to three textual presence glances and the
audio lane's mounted-or-idle transport state. Native tray commands route back
through the same discretion and audio callbacks used by the visible workspace.
The browser fallback detects that Tauri is absent and does not install the
bridge.

`RailAudioLaneController` owns the frontend's real `AudioLaneArbiter`; neither
the strip nor tray keeps a parallel playback state. The strip reflects source
capabilities for play/pause and volume, releases the holder for Skip, and
accepts the next ordered offer for Handoff. The tray receives the same
mounted-or-idle state, volume percentage, capability, and queued-offer count
through `v10.shell-bridge.4`, so it can accept an offer while the panel is
closed and disable actions the current lane cannot perform.

## Docked video sources

The video bay accepts official YouTube video URLs/IDs and Twitch channel or VOD
URLs. Inputs are normalized into a strict `VideoSourceSpec` with
`contentClass: 'docked-third-party'`; lookalike domains, malformed identifiers,
and Twitch clips are rejected before an iframe is created. YouTube uses the
official `/embed/{videoId}` player with autoplay disabled. Twitch uses the
official player with the current hostname as its required `parent`, autoplay
disabled, and a policy-selected initial mute. Below Twitch's official 400 × 300
minimum, the Rail shows a direct provider link instead of mounting a
non-compliant frame. The Rail hosts the provider player and does not moderate
the provider's content.

The connected workspace resolves the dock against the persisted daypart, channel
grant, and user-owned pin. At effective loudness 0 the provider stays unmounted.
At levels 1–2 it mounts muted and paused until the user acts; a transition down
into that posture immediately mutes and pauses an existing player without
remounting it. Deep work keeps the provider unmounted unless the user explicitly
pins ambient video. That pin is stored in V1 Rail state under the wellness
channel `v10.rail-docked-video` and moment `ambient-video`, not in local
storage. Loading or failed account policy is fail-closed. Provider autoplay
remains disabled even when level 3 or a deep-work pin permits the player to
mount.

The canonical source is saved under the versioned `oshun.v10.rail.video-dock.v1`
local-storage key, so both the browser fallback and native webview restore it as
a ready source after restart. This is a device preference, not an account or
lane authority: a restored source does not mount a provider or reacquire the
lane until the user selects **Use saved source**.

`RailVideoLaneController` owns the frontend's real `VideoLaneArbiter`, matching
the audio controller pattern. The panel's primary-holder ledger, waiting-offer
list, provider stage, and companion surface all project that one arbiter. A new
picker submission and **Use saved source** each become explicit user selection
actions; **Release** clears only the holder, while **Remove** also deletes the
device preference. Channel offers remain ordered and cannot promote themselves.

Protected first-party-live HLS/DASH is a separately injected authority. The lane
remains fail-closed by default and accepts a live offer only when its gate is
constructed for the canonical shared-media substrate. The panel mounts
`@oshun/live-media/browser/player`'s protected player only when the injected
playback resolver has the exact same authority ID, then rechecks the resolved
tenant, stream, protocol, API base, and manifest against the selected source
before any credentialed media is admitted. Playback starts only after **Watch
live**; **Release** tears down the browser player and viewer lease. The app's
default composition reads one optional, credential-free deployment manifest.
Without that manifest, the gate remains `not_configured`; the presence of client
wiring alone is not a production feature flip.

### First-party live deployment manifest

Set `NEXT_PUBLIC_RAIL_FIRST_PARTY_LIVE_CONFIG` only when the deployment has a
same-origin protected-playback API and exact canonical media bindings:

```json
{
  "authorityId": "veritas.rail-playback.v1",
  "apiBaseUrl": "/api/rail-live",
  "sources": [
    {
      "channelId": "v1.veritas-live",
      "sourceId": "evening-coverage",
      "tenantId": "veritas",
      "streamId": "11111111-1111-4111-8111-111111111111",
      "protocol": "hls",
      "manifestUrl": "https://media.example/live/evening/index.m3u8"
    }
  ]
}
```

The manifest contains no playback credential. It binds lane admission and the
browser player to the same authority and exact channel, source, tenant, stream,
protocol, and manifest. `apiBaseUrl` must be root-relative and expose the
canonical join, heartbeat, and leave routes backed by `@oshun/live-media`.
Viewer leases and playback grants remain memory-only.

Invalid, cross-origin, credentialed, partial, duplicate, or ambiguous
configuration fails loudly during composition. Enabling the variable does not
prove a program live: release readiness still requires a deployed tenant host,
canonical publication, entitlements, and retained media receipt.

The lane transport connects to YouTube's IFrame Player API or Twitch's
interactive embed API and reflects provider-reported ready, playing, paused,
buffering, ended, blocked, and offline states. Play, pause, and mute actions go
through those APIs. A small status poll reconciles native controls because
neither provider emits a dedicated mute-change event. If the interactive API is
blocked or the Twitch frame cannot meet its minimum size, the Rail disables its
transport and identifies the provider's own controls as the available path.

The deterministic Playwright suite substitutes provider SDKs so ordinary CI does
not depend on third-party uptime. A separate desktop-only probe keeps the
provider boundary honest by loading the real YouTube privacy-enhanced iframe and
official IFrame API while controlling only the Rail BFF policy responses:

```bash
RAIL_REAL_PROVIDER_E2E=1 pnpm exec playwright test e2e/video-dock.spec.ts \
  --project=desktop-chromium --grep 'live YouTube player' --workers=1
```

That opt-in probe requires internet access. It verifies API readiness, the
muted-and-paused posture at effective loudness 1, device-local persistence, no
automatic lane reacquisition after reload, and explicit **Use saved source**
handoff. It releases the provider before the automatic app-level axe scan so a
third-party iframe's internal DOM is not misreported as Rail-owned markup.

### Embed security and privacy review

This is the reviewed day-one boundary for docked YouTube and Twitch. A provider
is not contacted while no source is docked, while account policy is loading or
failed, at effective loudness 0, or while an unpinned source is held for deep
work. Once the player is allowed to mount, the browser connects directly to the
selected provider; OSHUN does not proxy or inspect the video stream.

What Rail sends intentionally:

- YouTube receives only the canonical 11-character video ID, `autoplay=0`,
  `playsinline=1`, `enablejsapi=1`, and the Rail origin when the enclosing page
  has a non-null HTTP(S) origin. Arbitrary query parameters from the pasted URL
  are discarded. The iframe always uses YouTube's `www.youtube-nocookie.com`
  privacy-enhanced domain. This reduces personalization of the embedded view and
  ads; it does **not** mean that no provider request, identifier, cookie, or ad
  can exist.
- Twitch receives only the canonical channel name or `v`-prefixed VOD ID, the
  current hostname in its required `parent` list, `autoplay=false`, and the
  policy-selected initial mute. Pasted query parameters are discarded. Clips are
  not accepted by this player.
- Neither provider URL nor player options contain the V1 bearer, OSHUN user ID,
  subscription/grant state, daypart, ambient pin, discretion preferences, or the
  original pasted URL. The saved provider reference remains in the device-local
  key described above. A reduced-size “Open on Twitch” link uses
  `noopener noreferrer` so that navigation does not send the Rail referrer.

What the provider can observe:

- The iframe receives an origin-only referrer under
  `strict-origin-when-cross-origin`; YouTube requires an HTTP referrer or an
  equivalent client identity, while Twitch requires the embedding hostname. The
  provider also receives ordinary network and client facts such as IP address,
  user agent/device characteristics, request time, cookies or similar
  identifiers allowed by the browser, and playback/interaction events. Provider
  account state may therefore apply if the viewer is already signed in there.
- The provider iframe is cross-origin and the browser same-origin policy keeps
  it from directly reading the Rail DOM or local storage. However, the official
  YouTube and Twitch JavaScript SDKs are remote scripts that execute in the Rail
  document itself. Like every allowlisted page script, they technically have
  app-origin access, including access to DOM state, web storage, and runtime
  globals. CSP limits where scripts can load from and where ordinary fetches can
  connect; it is not a confidentiality sandbox for an already allowlisted
  script. The Rail therefore treats both SDK origins as privileged supply-chain
  dependencies and permits no lookalike or wildcard script origin.

The native shell enforces its policy through `tauri.conf.json`: only
`www.youtube-nocookie.com` and `player.twitch.tv` may be framed, only the
official `www.youtube.com` and `player.twitch.tv` SDK origins may execute remote
scripts, and `frame-ancestors 'none'`, `object-src 'none'`, `base-uri 'self'`,
and `form-action 'self'` close unrelated embedding and injection paths.
`frame-src` controls what the Rail may embed; `frame-ancestors` independently
prevents another page from embedding the Rail. Provider-side frame-ancestor
rules remain controlled by YouTube and Twitch.

The browser fallback is a Next static export, so Next cannot attach response
headers itself. `public/_headers` ships the equivalent CSP, referrer policy,
`X-Content-Type-Options`, and legacy `X-Frame-Options` rule for static hosts
that support the `_headers` convention. Any other host **must** translate that
file into real HTTP response headers; `frame-ancestors` is not effective in a
`<meta>` tag. A custom `NEXT_PUBLIC_OSHUN_BFF_URL` outside localhost or
`*.oshun.app` also requires an explicit `connect-src` addition in both the host
policy and the packaged Tauri policy—do not widen it to bare `https:`.

Desktop webviews can expose a custom-protocol/null origin or omit an HTTP
referrer. The code omits an invalid `origin` player parameter and surfaces
provider rejection rather than inventing identity. YouTube error 153 or a Twitch
parent rejection during a packaged-platform release test is therefore a release
blocker for that target and must be solved with provider-compliant WebView
client identification, not by suppressing the error.

Review sources (checked 2026-07-17):

- [YouTube IFrame API and `origin` guidance](https://developers.google.com/youtube/iframe_api_reference)
- [YouTube privacy-enhanced mode](https://support.google.com/youtube/answer/171780)
- [YouTube embedded-player client identity](https://developers.google.com/youtube/terms/required-minimum-functionality)
- [Twitch embed requirements and `parent`](https://dev.twitch.tv/docs/embed/)
- [Twitch interactive video player parameters](https://dev.twitch.tv/docs/embed/video-and-clips/)
- [Tauri CSP behavior](https://v2.tauri.app/security/csp/)
- [W3C CSP `frame-src` and `frame-ancestors`](https://www.w3.org/TR/CSP/)
- [Google data from embedded services](https://policies.google.com/technologies/partner-sites)
- [Twitch privacy notice](https://legal.twitch.com/en/legal/privacy-notice/)

## Overlay / second-screen companion

`RailVideoOverlaySlot` is the reusable RA.4 surface for video that stays on an
external player or broadcast. Its dominant readout is the kernel-owned companion
clock. The user enters the external video's visible time and whether it is
playing or paused; the Rail can then pause/resume its own clock, apply a bounded
manual offset, or re-sync while paused. Copy and transport capabilities state
explicitly that these actions do not seek, control, inspect, caption, or
otherwise contact the external video.

A successful in-place pause or resume also updates the playback selector used by
the next re-sync. This keeps a paused clock paused across correction without
silently reusing a stale pre-toggle selection; a rejected clock action leaves
the form untouched.

The shared video panel mounts that slot only when its channel and source match
the selected overlay holder. A mismatch is visible as an unavailable companion
surface rather than rendering unrelated cues. The docked-source picker remains
available beneath an overlay so an explicit user selection can hand the single
lane to a saved or newly entered provider source.

The surface renders only reference cues already projected as active by
`VideoOverlaySlot`; the owning channel must resolve each reference into its real
content. It has an honest empty/unsynced state and no built-in sample claims.
The browser E2E harness uses clearly labeled contract-probe references solely to
verify control, responsive, and accessibility behavior. RB.1 remains the first
production consumer and will supply the Veritas claim ticker from its real
caption, scoring, and receipt pipeline. Provider-time integration is not
configured in Phase A and must not be inferred from the running manual clock.

The native screen-share bridge validates the shell's capability/status payload,
adapts supported ScreenCaptureKit observations to the R0.7 detector contract,
and ignores stale timestamps. Privacy settings report system, self-share-only,
initializing, unavailable, or detector-error coverage explicitly; unsupported
platforms never receive a simulated detector.

The web adapter takes command names, event names, and payload types from the
shared `@oshun/contracts/v10` shell-bridge schema. It validates native events,
tray snapshots, screen-share status, and deterministic window commands before
calling application handlers or crossing the Tauri invoke boundary.

Startup settings read the real native autostart registration before enabling the
control. The browser fallback keeps the switch disabled with an explicit
desktop-only explanation, and a failed native read or write never assumes that
the requested registration succeeded.

## Local dogfood journal

The footer is the Phase-A exit-test instrument: it shows docked hours over the
last seven days and whether the local journal is recording. A visible document
owns one heartbeat-bounded spectator session; hiding, unloading, or closing the
surface ends it. A second tab cannot acquire the same active lease, and a crash
can extend a session by no more than the 45-second stale window. The journal is
strictly versioned as `v10.rail-dogfood.1`, retains at most 35 days / 10,000
validated calm-metric events, and remains device-local.

The browser writes `oshun.v10.rail.dogfood.v1` in local storage and offers an
explicit **Export** action. Inside Tauri, the same validated snapshot is also
written to the platform app-data directory through `v10.shell-bridge.4`.
`mirrorRailMetricsToDogfood` wraps the primary kernel metric sink: only an event
accepted by that authority is dispatched to the local journal, so micro-act and
elevation summaries cannot bypass the canonical schema. Corrupt storage and
native write failures remain visible as **Journal error** and are not replaced
with an empty success state.

Run the report against Tauri's default local file or a browser export:

```bash
pnpm rail dogfood-report
pnpm rail dogfood-report --input ./rail-dogfood-journal.json --json
```

The report clips docked sessions to the exact seven-day window and summarizes
spectator/player hours, micro-act outcomes, and granted/denied elevation
decisions by channel. It has no remote or engagement-metric path.

## Case Files fairness receipt

After a `case.revealed` delivery, `RailWorkspace` can mount the strict Case
Files receipt ledger. Its **Verify locally** action reconstructs the same
recursively key-sorted UTF-8 payload bytes used by the daily cook, imports the
key-id-matched SPKI public key from a caller-supplied trusted registry, and runs
Ed25519 through browser Web Crypto. The receipt never supplies its own trust
root and the check does not make a network request. Malformed receipts, missing
trusted keys, post-signing changes, and unavailable browser verification remain
visibly unverified and fail closed.

Production composition must source the public-key registry from pinned client
configuration or an equivalently governed trust path, separately from the
untrusted receipt delivery. Unit and desktop/mobile Chromium coverage use real
Ed25519 keys and signatures; the browser harness's generated registry is test
fixture material only.
