Context. surface customer · domain scene · route /scene/[id] · auth anon · source apps/oshun/web/src/app/scene/[id]/page.tsx
Last walked. 2026-06-28 focused automated addendum — default public viewer contract, no-reduced-motion media layer, materialized APNG source, visible mark, share snippet values, scene-URL clipboard success, clipboard-denied fallback, Report link, and download-grant form contract covered in scene-viewer-deepening.spec.ts; existing scene-password-and-grants.spec.ts and living-scene-card.spec.ts evidence reconciled against the previously unchecked password, tombstone, tenant-auth, reduced-motion, metadata, embed, grant, and takedown states below. 2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data (2xx), console/page-errors, expected content, screenshot verified; live screen-reader, touch, offline, and telemetry-delivery checks pending a manual AT pass. Evidence: WALKTHROUGH/results/runtime-sweep-2026-05-29.md; content re-verified 2026-06-03 against current source
Purpose#
Anonymous-accessible viewer for a shared "living offering" (a public
short-code-addressed scene). Resolves the share at request time via
fetchPublicLivingScene(params.id, { unlockToken }), where unlockToken comes
from resolveUnlockToken(params.id) — a read of the per-share HttpOnly cookie
set by the password gate (page.tsx ~15, 22, 57). It then renders one of four
terminal layouts based on resolution.kind: password challenge, tombstone,
tenant-auth required, or playable scene with accessibility panel and download
grant.
/scene is in apps/oshun/web/src/proxy.ts PUBLIC_PREFIXES (~line 59) —
anyone with the share code can open it.
Entry points#
- Direct URL via short code:
/scene/<id> - Email / SMS / social-share link with the URL
- QR code printed alongside a physical artifact
/scene/[id]/report"Back to offering" link- Redirect back from the
unlockSceneActionserver action after a successful or failed password submit (lands on/scene/<id>or/scene/<id>?error=…) - Open-Graph / Twitter preview tap from a social card (uses the metadata block
built in
generateMetadata)
Layout regions#
page.tsx resolves fetchPublicLivingScene(params.id, { unlockToken }) (token
from the per-share cookie) and branches on resolution.kind:
kind === 'password-required'#
Single <main> on dark teal background.
- Eyebrow:
Private Living Offering(mono caps, teal) - Title:
Password required(5xl serif) - Error alert (conditional,
role="alert"): rendered whensearchParams.errorisinvalid-password("That password did not unlock this offering.") ormissing-password("Enter the password shared with you.") - Server-action form (
<form action={unlockSceneAction}>, imported from./unlock-actions): a hidden<input name="shortCode" value={params.id}>, a required<input name="password" type="password" autoComplete="off">, and an "Open offering" submit. There is nomethod="get"and no?password=in the URL — the action exchanges the password for a short-lived unlock token via the BFF and stores the token in an HttpOnly, path-scoped cookie, then redirects back to/scene/<id>(or?error=…on failure)
kind === 'tombstone'#
- Eyebrow:
Living Offering <shortCode>(mono caps, slate) - Title:
resolution.message(typically "This offering has been retired" copy) - Sub-paragraph: explains that share URL, preview card, embed, and download grants are no longer available
kind === 'tenant-auth-required'#
- Eyebrow:
Tenant Living Offering <shortCode>(teal) - Title:
Tenant access required(5xl serif) - Body paragraph:
resolution.message - Tenant branding attribution:
resolution.tenantBranding.attributionCopyrendered as a smaller line when present
kind === 'playable' (full viewer)#
Linear-gradient near-black background, three rows (grid-rows-[auto_1fr_auto]).
- Header: tenant branding (logo or coloured dot) + attribution copy
- "Open OSHUN"
<Link href="/">
- "Open OSHUN"
- Media section (left column on
lg:breakpoint):- Motion layer: linear-gradient + cover-image background
- Reduced-motion artifact: hidden by default; revealed by
@media (prefers-reduced-motion: reduce); shows "Reduced-motion carousel · N segments" - Signed materialized media: an
<img>pointing at the BFF/v1/living-scenes/public/<shortCode>/mediaendpoint. The current V1 renderer ships APNG bytes; the page deliberately bypasses Next image optimisation so the browser receives the same bytes the C2PA media hash signed. - Visible-mark short code overlay (bottom-right,
[data-living-scene-visible-mark])
- Aside (right column on
lg:):- Eyebrow "Attested public view" in
accentcolour - Title (
resolution.openGraph.title) + description <dl>metadata grid: Privacy, Rendition, C2PA manifest id, Media hash (truncated to 24 chars), Sound ("User initiated")- Share settings panel (
[data-scene-share-settings]) with copy controls for:- Scene URL (
/scene/<shortCode>) - Literal iframe snippet:
<iframe src="/scene/<shortCode>/embed" width="960" height="540" loading="lazy" allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe> - oEmbed endpoint (
/scene/<shortCode>/oembed), which proxies the BFF embed response'soEmbedbody
- Scene URL (
- Accessibility panel (
[data-living-scene-accessibility-panel],[data-living-scene-viewer-chrome]) with data attributes:data-honors-reduced-motiondata-honors-high-contrastdata-honors-large-textdata-assistive-tech-navigable- Caption track summary
- Audio-description track summary
- Transcript artifact (download link if available)
- Keyboard cue buttons (one per
accessibility.viewerChrome.keyboardCueAccess)
- Action row: "Report" Link →
/scene/[id]/report(uses accent background); Download-grant POST form to the BFF/v1/living-scenes/public/<shortCode>/download-grants
- Eyebrow "Attested public view" in
- Footer:
- "Autoplay with sound is disabled."
Robots: <robotsMeta>
States#
- Password required — no valid unlock-token cookie → password gate
- Password error —
?error=invalid-passwordor?error=missing-password(set byunlockSceneAction's redirect) → password gate with therole="alert"message - Password accepted (playable) — the action issued an unlock-token
cookie; the next request resolves to
playable→ playable layout - Tombstone — share is retired; tombstone copy + no further affordances
- Tenant-auth required — tenant gating in effect; tenant attribution shown if branding exists
- Playable (default branding) — no tenant branding; accent defaults to
#7dd3c7; coloured dot in place of logo - Playable (tenant branding) — branding
logoUrlandattributionCopypresent; accent comes from branding - Reduced motion —
[data-living-scene-motion-layer]hidden;[data-living-scene-reduced-motion-artifact]revealed (via@media (prefers-reduced-motion: reduce)in the inline style block) - High contrast — viewer chrome gets a 2px outline (via
@media (prefers-contrast: more)) - High-DPI (2dppx) — accessibility panel font scales to 1.05rem
- No transcript / caption / audio description — accessibility panel shows "unavailable"
- No download grants — form button still rendered (POST will respond with whatever the BFF returns)
- Indexable vs noindex —
metadata.robots.{index, follow}is!resolution.noindexfor playable; non-playable kinds forcenoindex - Offline — service-worker cache: verify; the page is anon and relatively cacheable, but the renderable is request-time
- Standalone PWA — verify behaviour when launched from PWA shell
Interactions#
Password gate (kind === 'password-required')#
- Password input (input, type=password, name=password, required,
autoComplete=off) + hidden
shortCodeinput- Keyboard: standard
- Touch target: ≥ 44px confirm
- "Open offering" (submit button) — submits the
<form action={unlockSceneAction}>server action; on success the action sets the unlock-token cookie and redirects to/scene/<id>, on failure to/scene/<id>?error=invalid-password(ormissing-password). No password is ever placed in the URL.
Tombstone / tenant-auth — no actions#
Playable header#
- Tenant logo / dot (
aria-hidden) — visual only - "Open OSHUN" (Link,
href="/") — navigates to home
Playable media#
- Materialized APNG image —
<img aria-label={title} alt={title}>loads/v1/living-scenes/public/<shortCode>/mediaviabuildOshunBffUrl. The bytes should match the share's C2PA media hash and keep the visible-mark overlay in place. - Visible-mark badge (
[data-living-scene-visible-mark]) — non-interactive
Playable share settings#
- Copy scene URL — button with
data-scene-share-settings-copy="scene-url"writes the absolute/scene/<shortCode>URL to the clipboard and announcesScene URL copiedin[data-scene-share-settings-status]; clipboard-denied browsers showCopy unavailable - Copy iframe snippet — button with
data-scene-share-settings-copy="iframe"writes the literal iframe HTML for/scene/<shortCode>/embedand announcesIframe copied - Copy oEmbed endpoint — button with
data-scene-share-settings-copy="oembed"writes the absolute/scene/<shortCode>/oembedURL and announcesoEmbed copied - oEmbed route —
GET /scene/<shortCode>/oembedreturns the BFFoEmbedJSON for allowed public/private-link shares; denied embeds return{ reason: 'embed-unavailable' }with the upstream status
Playable accessibility panel#
- Transcript download link (anchor) — when present, points to
buildOshunBffUrl('/v1/living-scenes/public/<shortCode>/transcripts/<locale>') - Keyboard cue buttons (button,
type="button") — one peraccessibility.viewerChrome.keyboardCueAccessentry; verify handler bindings
Playable actions#
- "Report" link (Link) →
/scene/<id>/report - "Download grant" button (submit inside
<form action={url} method="post">) — POSTs to/v1/living-scenes/public/<shortCode>/download-grants
Data & contracts#
- Reads:
resolveUnlockToken(params.id)— reads the per-share unlock-token cookie (sceneUnlockCookieName(shortCode), defined in./unlock-actions.shared)fetchPublicLivingScene(params.id, { unlockToken })from@/lib/living-scenes-public— returns one ofpassword-required | tombstone | tenant-auth-required | playable
- Writes:
- Password gate:
unlockSceneAction(server action,./unlock-actions) →POST /v1/living-scenes/public/<shortCode>/unlock-tokenviaexchangePasswordForSceneUnlockToken; on success writes the HttpOnly unlock-token cookie - "Download grant" form POST →
/v1/living-scenes/public/<shortCode>/download-grants(built viabuildOshunBffUrl) - Share-settings copy buttons write to the browser clipboard only
- oEmbed endpoint:
GET /scene/<shortCode>/oembedproxies/v1/living-scenes/public/<shortCode>/embed
- Password gate:
- Realtime: none from the page itself
- Caching: request-time fetch (server component without explicit cache directive); verify default Next.js fetch policy
- Auth/role check: anon (
/sceneis insrc/proxy.tsPUBLIC_PREFIXES, ~line 59) - Metadata (
generateMetadata):- Playable:
title: resolution.openGraph.title(bare, ~36),description,robots: { index: !noindex, follow: !noindex }, OpenGraphtype: 'video.other'with image, Twittersummary_large_image - Non-playable kinds:
title: 'Living Offering Retired'(bare, ~29),robots: { index: false, follow: false } - The
| OSHUNsuffix seen in the browser tab is not in this file — it is applied by the root layout'smetadata.title.template = '%s | OSHUN', which wraps whatever bare title these branches return
- Playable:
Automation Evidence#
apps/oshun/web/e2e/scene-viewer-deepening.spec.tsan indexable public scene head carries OG, Twitter card, and indexable robotsunder prefers-contrast:more the viewer still renders its branding and visible markdefault public viewer exposes motion media, actions, snippets, and scene URL copyclipboard-denied browsers surface an explicit copy-unavailable status- content-addressed share idempotency for lost-response retries
apps/oshun/web/e2e/scene-password-and-grants.spec.ts- password-protected share creation, password gate, invalid/missing password alerts, correct unlock cookie, expired-token fallback, password embed break-out, download-grant redemption/media hash, report edge cases, rate limiting, and operator takedown cascade.
apps/oshun/web/e2e/living-scene-card.spec.ts- noindex public attestation, tenant branding, reduced-motion swap, iframe and oEmbed copy, playable embed, retired tombstone, and tenant-auth-required branch.
Cross-references#
- Sibling scene routes:
- Public-path gating:
apps/oshun/web/src/proxy.tsPUBLIC_PREFIXES(/scene~line 59); also documented inshell/04-auth-session.md - Component sources:
apps/oshun/web/src/app/scene/[id]/page.tsx(PublicLivingScenePage,resolveUnlockToken,generateMetadata)apps/oshun/web/src/app/scene/[id]/ShareSettingsPanel.tsx(public URL, iframe snippet, and oEmbed copy controls)apps/oshun/web/src/app/scene/[id]/unlock-actions.ts(unlockSceneActionserver action) +unlock-actions.shared.ts(sceneUnlockCookieName)apps/oshun/web/src/lib/living-scenes-public.ts(fetchPublicLivingScene,exchangePasswordForSceneUnlockToken)apps/oshun/web/src/lib/bff-endpoints.ts(buildOshunBffUrl)
Open questions / known gaps#
- Verify that the four keyboard cue buttons have real handlers (currently
type="button"with noonClick) - Resolved: the password gate is a POST server action (
unlockSceneAction), not a GET form — the password is never reflected in the URL; it is exchanged for a short-lived unlock token stored in an HttpOnly cookie, and only?error=…is ever surfaced in the URL - Confirm whether
fetchPublicLivingSceneis cacheable per share code (it's hit on every request, keyed by the unlock-token cookie) - Document the
accessibility.viewerChrome.*boolean meanings and whether failing checks affect rendering - Confirm Open-Graph / Twitter image URLs return real assets for
openGraph.imageUrlandtwitterCard.imageUrl