# Journey: Scene · anonymous viewer at oshun.app/scene/<id>

The public Living Offering viewer is a real branch-driven server surface, not a
single happy-path player. It distinguishes password, tenant, retired, transient
outage, and playable outcomes; the playable branch serves attested APNG bytes,
accessibility artifacts, embeds, reports, and short-lived downloads. Durable
share metadata does not make every runtime artifact durable: materialized media
bytes and download grants remain process-local.

## Personas

- **Anonymous recipient** — follows a public or private-link URL without an
  Oshun session.
- **Password recipient** — exchanges a shared password once, then revalidates
  through a short-lived HttpOnly unlock cookie.
- **Tenant recipient** — reaches a tenant-internal link and must enter through a
  tenant-aware surface rather than the anonymous viewer.
- **Crawler or embed host** — consumes robots/Open Graph/Twitter or embed/oEmbed
  metadata without receiving privileged content.
- **Accessible-media user** — relies on reduced motion, high contrast,
  transcript, caption, or audio-description metadata.

## Pre-conditions

- `/scene/<shortCode>` is a public middleware path. The page server-fetches
  `GET /v1/living-scenes/public/:shortCode` with `cache: 'no-store'`.
- A playable share requires a retained artifact, active share state, and the
  correct password/tenant conditions.
- When Postgres is configured, artifact/share/index/audit/takedown state is
  wired through `wireDurableLivingSceneShares`. Without that wiring, the same
  stores are process-local.
- The share's robots policy determines whether the playable page is indexable;
  protected and non-playable branches are always noindex.

## Steps

### 1. Resolve the public branch

The server resolves exactly one of the following outcomes:

| Branch      | Viewer and recovery contract                                                  |
| ----------- | ----------------------------------------------------------------------------- |
| Playable    | Attested media, transcript, report, download, and copy actions render.        |
| Password    | A successful exchange sets a short-code-scoped unlock cookie.                 |
| Tenant auth | Tenant-access message; no anonymous media or download surface.                |
| Tombstone   | Retired offering after a definitive 4xx, missing, revoked, or deleted result. |
| Unavailable | Temporary 5xx/network outage with a real retry, never a false retirement.     |

These rows map directly to the BFF resolution kinds `playable`,
`password-required`, `tenant-auth-required`, `tombstone`, and `unavailable`.

`generateMetadata` uses the share's Open Graph/Twitter/robots values only for a
playable branch. All non-playable branches currently share the generic title
“Living Offering Retired,” including password, tenant, and transient-unavailable
states; that metadata mismatch is a known gap.

### 2. Unlock a password-protected offering

The raw password is posted to the unlock-token endpoint. A valid exchange mints
an HMAC-signed, short-code-scoped token with a four-hour lifetime; the server
action stores it in an HttpOnly path-scoped cookie. Wrong, missing, expired, or
cross-scene tokens return to the password gate without leaking the media.

### 3. Render the playable offering

The page displays tenant/default branding, public mark, privacy/rendition, C2PA
manifest and media-hash summary, and the exact materialized APNG from `/media`.
Reduced-motion CSS swaps the motion layer for the reduced artifact;
high-contrast CSS adds a stronger viewer outline. Captions, audio-description
metadata, and transcript download are exposed from the share resolution.

The keyboard-cue labels are rendered as buttons, but those buttons have no
handlers. They are descriptive chrome today, not working playback shortcuts.

### 4. Copy, embed, download, or report

`ShareSettingsPanel` copies the scene URL, iframe, and oEmbed snippets and
surfaces an explicit clipboard-denied state. Download creates a 15-minute grant
and redeems the same APNG bytes whose hash is carried by the C2PA manifest. The
report link opens the public abuse form; it does not require the viewer to be
signed in.

### 5. Revisit after lifecycle changes

Revocation, originating-user deletion, or operator takedown turns the viewer
into a tombstone and invalidates existing grants. A transient BFF failure stays
an unavailable/retry state. The service worker treats API reads as network-only
by default, so a first online visit does not establish a supported offline
viewer guarantee.

## Post-conditions

- A playable viewer sees only the public artifact, branding, attestation, and
  accessibility artifacts authorized by that share.
- Password material is replaced by a scoped unlock token; the raw password is
  not stored in the cookie.
- A valid download grant redeems attested media while the share is active and
  the grant is unexpired.
- The viewer never claims an outage is a retirement, but its head metadata does
  not yet express every non-playable branch accurately.

## Failure modes

- **Restart after durable share hydration** — share/artifact metadata can
  survive, but `shareMediaById` and `downloadGrantsById` are process-local maps
  and are cleared on hydration. The viewer can resolve as playable while
  `/media` returns `media_not_materialized`; old grants never survive restart.
- **Invalid/expired/cross-scene unlock** — returns to the password gate.
- **Tenant-internal link without tenant context** — renders “Tenant access
  required,” never public media.
- **Clipboard denied** — copy controls report an explicit unavailable status.
- **Offline revisit** — no supported cached media/API contract; the request may
  fail even after an earlier visit.
- **Accessibility cue activation** — the visible cue buttons currently do
  nothing.
- **View analytics** — there is no per-IP or per-share public view counter in
  this path; do not infer audience telemetry from page rendering.

## E2E coverage

- [`apps/oshun/web/e2e/living-scene-card.spec.ts`](../../apps/oshun/web/e2e/living-scene-card.spec.ts)
  covers public attestation/noindex/tombstone, reduced motion, embed/copy,
  tenant gating, and delete cascade.
- [`apps/oshun/web/e2e/scene-password-and-grants.spec.ts`](../../apps/oshun/web/e2e/scene-password-and-grants.spec.ts)
  covers password gates, scoped/expired tokens, embed protection, APNG grant
  redemption with matching media hash, report access, and deletion invalidation.
- [`apps/oshun/web/e2e/scene-viewer-deepening.spec.ts`](../../apps/oshun/web/e2e/scene-viewer-deepening.spec.ts)
  covers indexable metadata, high contrast, playable viewer actions, clipboard
  denial, and idempotent re-share behavior.
- **Coverage depth: deep** for the online browser/API branches. Restart media
  recovery, offline replay, and functional cue shortcuts remain uncovered.

## Per-view files touched

- [`customer/12-scene/scene-id.md`](../customer/12-scene/scene-id.md) — all
  viewer branches and playable chrome.
- [`customer/12-scene/scene-id-report.md`](../customer/12-scene/scene-id-report.md)
  — report continuation.
- [`shell/03-pwa-behavior.md`](../shell/03-pwa-behavior.md) — network/cache
  boundary.
- [`shell/04-auth-session.md`](../shell/04-auth-session.md) — anonymous,
  password-token, and tenant-access distinctions.

## Cross-references

- [`scene-keep-and-share.md`](./scene-keep-and-share.md) — creation and
  revocation before/after this viewer.
- [`public-scene-abuse-report.md`](./public-scene-abuse-report.md) — public
  report continuation.
- [`offline-first-time-use.md`](./offline-first-time-use.md) — why API
  network-only policy matters.
- Sources: `apps/oshun/web/src/app/scene/[id]/page.tsx`,
  `apps/oshun/web/src/lib/living-scenes-public.ts`, and
  `apps/oshun/bff/src/routes/living-scenes.ts`.

## Open questions

- Should materialized media be snapshot/object-store durable, or rebuilt and
  re-hashed deterministically when a durable share hydrates?
- Should non-playable metadata distinguish password, tenant, retired, and
  transient-unavailable states?
- Should keyboard-cue buttons control playback, or render as non-interactive key
  labels until a controller exists?
- Is an explicit offline public-viewer product contract wanted, including
  revocation freshness and sensitive-cache policy?
