V1 Web PWA · Journey

Journey: AAA upgrade entitlement bounce

A source-reconciled journey record for V1 Web PWA — shipped behavior, state boundaries, failure modes, and the automation evidence available today.

automated · 2 specs verdict: partial· 2026-05-29
9sections7 minread

On this page
Journey at a glance
ActorsContemplative customer with AAA entitlementContemplative customer without AAAOperator-adminNon-AAA customer on a hub page
Automation2 E2E spec(s) · verdict: partial· 2026-05-29

Journey flow#

Generated from the authored steps below — click a node to jump to that section.

flowchart TD s1["1. Customer attempts a direct nav to an AAA-o…"] s2["2. Branch A — AAA tier present, render-yemaya…"] s3["3. Branch B — no AAA, hard-block-404"] s4["4. /aaa-upgrade renders the handoff (Branch A…"] s5["5. Customer chooses one CTA"] s6["6. Embedded-leak case — fail-closed legacy ro…"] s1 --> s2 --> s3 --> s4 --> s5 --> s6 click s1 href "#1-customer-attempts-a-direct-nav-to-an-aaa-only-surface" click s2 href "#2-branch-a--aaa-tier-present-render-yemaya-cta" click s3 href "#3-branch-b--no-aaa-hard-block-404" click s4 href "#4-aaa-upgrade-renders-the-handoff-branch-a-continuation" click s5 href "#5-customer-chooses-one-cta" click s6 href "#6-embedded-leak-case--fail-closed-legacy-route-plus-aaagate-defence-in-depth" s6 --> v(["verdict: partial"]) click v href "../results/aaa-upgrade-entitlement-bounce.html" style v stroke:#d6a531,color:#d6a531

A signed-in customer hits an AAA-only generation surface on the contemplative product (/studio/isis/<aaa>). proxy.ts runs resolveStudioBoundary; if the user has aaa-creator or operator-admin, they are redirected with status 307 to /aaa-upgrade?from=<orig>; if not, they get a hard 404. This is the cross-surface handoff from the contemplative route boundary into the Yemaya Studio surface.

Personas#

  • Contemplative customer with AAA entitlement — primary path; sees the disclosure and chooses Yemaya Studio or returns to the gallery
  • Contemplative customer without AAA — hard-blocked at the proxy; 404 on any direct nav to an AAA-only segment
  • Operator-admin — also passes the boundary (operator-admin is the second allowed tier in resolveStudioBoundary)
  • Non-AAA customer on a hub page — the proxy can't see inside an embed; AaaGate plugs the leak per-embed inside hubs like StudioIsisJobEnvelopeWorkspace

Pre-conditions#

  • User signed-in (OSHUN_ACCESS_TOKEN_COOKIE_NAME = 'oshun-access' set)
  • readCustomerStudioEntitlement() can decode the access token (3-segment prod JWT or 2-segment dev token); malformed falls back to anon
  • Target segment is in AAA_ONLY_STUDIO_ROUTES (frozen list in libs/isis/entitlements/src/studio-boundary.ts — includes gpu-worker, lora-training, comfyui-nodes, model-merging, animatediff-lightning, rife-interpolation, etc.)
  • resolveGenerationTier(entitlement) returns one of contemplative, curated-creator, aaa-creator, operator-admin

Steps#

1. Customer attempts a direct nav to an AAA-only surface#

  • Customer follows a deep link or types /studio/isis/gpu-worker
  • Proxy receives the request; pathname.startsWith(STUDIO_ISIS_PREFIX) is true; segment extracted via pathname.slice(STUDIO_ISIS_PREFIX.length).split('/')[0]
  • AAA_ONLY_STUDIO_ROUTES.includes(segment) is true
  • Proxy calls resolveStudioBoundary({ routeSegment, entitlement })

2. Branch A — AAA tier present, render-yemaya-cta#

  • Tier is 'aaa-creator' or 'operator-admin'
  • resolveStudioBoundary returns { action: 'render-yemaya-cta', disclosureCopy, upgradeUrl: '/aaa-upgrade' }
  • applyStudioBoundary returns { kind: 'render-yemaya-cta', upgradeUrl }
  • studioBoundaryResponse builds NextResponse.redirect(url, 307) with url.pathname = '/aaa-upgrade' and url.searchParams.set('from', request.nextUrl.pathname)
  • Headers: X-Request-Id: <id>, X-Studio-Boundary: aaa-cta
  • Browser follows the 307 to /aaa-upgrade?from=/studio/isis/gpu-worker

3. Branch B — no AAA, hard-block-404#

  • Tier is 'contemplative' or 'curated-creator'
  • resolveStudioBoundary returns { action: 'hard-block-404' }
  • studioBoundaryResponse rewrites to /_not-found (NextResponse.rewrite(notFoundUrl, { status: 404, headers: { 'X-Request-Id', 'X-Studio-Boundary': 'hard-block' } })) so the 404 ships with the root document shell (<html lang="en"> + <title>) for WCAG document-title / html-has-lang — not a bare text/plain "Not Found" body
  • No leakage of /aaa-upgrade existence for this segment
  • Verify: studio/domain-bridges/isis/studio-isis-gpu-worker.md gated state (hard-block branch)

4. /aaa-upgrade renders the handoff (Branch A continuation)#

apps/oshun/web/src/app/aaa-upgrade/page.tsx is a server async component that reads searchParams.from, sanitises it to string | null, and renders inline-styled chrome (no ShellLayout).

  • H1 "This surface lives in Yemaya AAA Studio"
  • Body "Oshun keeps the contemplative product curated — raw graph editors, LoRA training, model merging, and the rest of the AAA-tier generation surfaces live in Yemaya AAA Studio. Your entitlement includes access."
  • "You arrived from /studio/isis/gpu-worker" conditional line (only when from is a string)
  • Primary Link data-action="open-yemaya"/studio; secondary data-action="back-to-gallery"/studio/generation-gallery
  • data-testid="aaa-upgrade-page" on <main>
  • Verify: customer/13-system/aaa-upgrade.md default + ?from states; CTA labels; touch targets

5. Customer chooses one CTA#

  • "Continue in Yemaya Studio" → /studio (cross-surface handoff; the contemplative product never embeds the AAA generation surface itself)
  • OR "Back to gallery" → /studio/generation-gallery (returns to curated surface)

6. Embedded-leak case — fail-closed legacy route plus AaaGate defence-in-depth#

The legacy /studio/isis/* tree now fail-closes at the route boundary before a non-AAA user can reach a hub that embeds an AAA workspace. AaaGate remains the component-level defence for any future reachable parent surface where the proxy cannot see inside the embed.

  • Route-level boundary: /studio/isis/job-envelope returns the same hard-block 404 for contemplative users, preserves the URL, and does not render an in-place blocked card
  • Component tree: StudioIsisWorkflowRegistryWorkspace mounts <AaaGate routeSegment="gpu-worker"><StudioIsisGpuWorkerWorkspace /></AaaGate> (same pattern in Job Envelope, Web, Output Registry, AI Video, etc.)
  • AaaGate is server-side; calls readCustomerStudioEntitlement() from cookies, runs resolveGenerationTier(entitlement).tier
  • For aaa-creator / operator-admin: renders children
  • For contemplative / curated-creator: renders AaaBlockedCard (data-aaa-gate-blocked, data-aaa-gate-route-segment="gpu-worker") with eyebrow "AAA tier required", route-segment label, and a Link to /aaa-upgrade ("Open the upgrade page")
  • Unknown segments (not in AAA_ONLY_STUDIO_ROUTES) render children (defensive default)
  • BFF independently enforces entitlement — the gate is UI-only
  • Verify: studio/domain-bridges/isis/studio-isis-gpu-worker.md "AAA-gated parent embedding non-AAA-gated Generation API child" open question — captures the inverse case to this one

Post-conditions#

  • AAA-tier user: at /studio (or back at /studio/generation-gallery after declining)
  • Non-AAA user: at a 404 for direct nav and the legacy hub; any future reachable hub embed should show an in-place AaaBlockedCard; the user never sees the AAA surface or leaked state
  • Telemetry: the /aaa-upgrade client emits aaa_upgrade_viewed, aaa_upgrade_open_yemaya_clicked, and aaa_upgrade_back_to_gallery_clicked; the proxy boundary remains observable through X-Studio-Boundary

Failure modes#

  • Cookie missing / malformeddecodeEntitlement falls back to anon; user sees the 404 (correct posture, not the disclosure)
  • Unknown segment wrapped in AaaGate — defensive default renders children
  • Segment removed from AAA_ONLY_STUDIO_ROUTES — proxy + AaaGate both fall through to render-as-is; verify the bridge change is intentional
  • Open redirect via ?from=from renders as plain <code> content, no <a href>; Next escapes; CTAs go to fixed destinations only
  • Cross-domain leak — entitlement decoder is import 'server-only'; never reaches client bundles
  • BFF skew — the gate is UI-only; if BFF rules drift from AAA_ONLY_STUDIO_ROUTES, a non-AAA user could reach data through the API — covered by entitlement-equivalence tests

E2E coverage#

  • apps/oshun/web/e2e/aaa-upgrade-entitlement-bounce.spec.ts — steps 1–6: asserts the 307 render-yemaya-cta (with X-Studio-Boundary: aaa-cta and from=), the contemplative hard-block 404 (URL preserved, WCAG doc shell, no /aaa-upgrade leak), the disclosure page + both CTA destinations, aaa_upgrade_viewed, both CTA click telemetry events, the fail-closed legacy /studio/isis/job-envelope hub denial, and the ?from= open-redirect failure mode (echoed as inert <code>, never an <a>), plus mobile standalone rendering with safe source propagation/no overflow/≥44px CTAs and real service-worker offline document replay of the warmed /aaa-upgrade page.
  • apps/oshun/web/e2e/entitlements-aaa-bff.spec.ts — the BFF entitlement resolver behind the proxy bounce (the journey's Open question #4) over real HTTP: GET /v1/entitlements/aaa returns entitled for aaa-creator, bounces contemplative toward /aaa-upgrade, and 401s without a bearer token — so a drift between the proxy.ts boundary and the BFF's own enforcement is now detectable.
  • Coverage depth: deep — see coverage.md.
  • Uncovered: proxy-side structured access-log telemetry for redirect / 404 decisions remains a backend observability gap; the browser aaa_upgrade_* telemetry is covered. The operator-admin second-tier path is asserted via tier-equivalence in the boundary unit tests rather than re-driven here (same render-yemaya-cta outcome as aaa-creator).

Per-view files touched#

Cross-references#

  • V1/features.md § Crypto Payments — Non-Custodial Entitlement Settlement (upstream path that grants the AAA tag in the first place)
  • V1/ARCHITECTURE.md § Security, Privacy, and Compliance (entitlements interlock with DomainAuthPolicy.scopes)
  • apps/oshun/web/src/proxy.tsapplyStudioBoundary, studioBoundaryResponse
  • apps/oshun/web/src/lib/server/studio-entitlement.tsreadCustomerStudioEntitlement (server-only token decoder)
  • libs/isis/entitlements/src/studio-boundary.tsAAA_ONLY_STUDIO_ROUTES, isAaaOnlyRoute, resolveStudioBoundary, StudioBoundaryOutcome
  • apps/oshun/web/src/components/studio/AaaGate.tsx — defence-in-depth embed gate
  • apps/oshun/web/src/app/aaa-upgrade/page.tsx — disclosure page source
  • Sibling journeys: sign-up-and-pay-crypto.md (how the user got the AAA tag), nyx-to-tara-bridge.md (sibling cross-domain handoff pattern)

Open questions / known gaps#

  • /aaa-upgrade emits aaa_upgrade_viewed, aaa_upgrade_open_yemaya_clicked, and aaa_upgrade_back_to_gallery_clicked; the Playwright journey captures all three events and verifies their safe source/destination payloads
  • Proxy has no telemetry on the redirect / 404 path itself; response headers (X-Studio-Boundary) are the only signal; consider a structured access-log event
  • Page chrome uses raw inline styles, not Lilith design-system primitives; keyboard focus outlines, ≥44px CTA targets, reduced-motion static state, mobile standalone containment, and offline replay are now covered by the route-level Playwright assertions. The design-system primitive migration remains a visual-design gap, not an unverified interaction.
  • Document the BFF-side entitlement check that mirrors AAA_ONLY_STUDIO_ROUTES, so a drift between proxy + BFF is detectable (the comment in AaaGate.tsx says "The BFF still independently enforces entitlement on every request" — entitlements-aaa-bff.spec.ts captures the real HTTP resolver)
  • Per-view file flags "AAA-gated parent embedding non-AAA-gated Generation API child — confirm intended UX"; the embedded-leak AaaGate pattern is the inverse (non-AAA parent embedding AAA child). The journey now documents the fail-closed legacy hub behavior and the real spec asserts no embedded blocked card leaks after route-level denial.