> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# One-origin security model (design)

> Threat model, cookie scoping, CSP, service-worker and storage rules that gate an app joining tuturuuu.com/<app>.

> [!NOTE]
>
> **Status: proposed, phase 1 (design only).** This page gates admission to the
> [one-origin super-app](https://docs.tuturuuu.com/platform/architecture/one-origin-super-app.md). An app may
> not be routed onto `tuturuuu.com` until it meets every admission requirement below
> with exact-commit evidence. Open critical/high findings hold the release, per the
> repository security default.

## The core trade

The browser's security boundary is the **origin** (scheme + host + port). Paths are
not a boundary. Once Meet, Mail and Web share `https://tuturuuu.com`, script running
in any one of them can do all of the following:

* `fetch` any same-origin URL with the user's cookies and **read the response**
  (`/api/*`, `/mail/api/*`). CORS no longer applies.
* Open or frame another app's page (`window.open('/mail/...')`) and script its DOM.
  `frame-ancestors` stops framing, but it cannot stop `window.open` from same-origin
  script.
* Read and write every app's `localStorage`, `sessionStorage`, IndexedDB and Cache
  Storage, and enumerate or unregister service workers.
* Use any permission the user granted to the **origin**. A camera or microphone
  grant for Meet also covers a script injected into Mail.

**Therefore: an XSS in any mounted app is an XSS in all of them, Web's dashboard
included.** The session is shared by design. Per-app CSP and frame rules reduce
accidents and raise attacker cost. They do not restore isolation. The rest of this page is about
keeping XSS out and limiting what a successful one can reach.

### What changes compared with subdomains

The subdomain model already shares a lot:

* The Supabase auth cookie is set with `Domain=.tuturuuu.com`, so every subdomain
  receives it.
* Any subdomain can set (toss) a cookie for the parent domain.
* Supabase SSR cookies are script-readable by default. **Verify** whether ours are.
  If they are, an XSS on any subdomain can already steal the session.

One origin adds the four capabilities listed above: response reading, storage,
same-origin window scripting and shared permission grants. One origin also enables
something the subdomain model cannot: host-only `__Host-` cookies that subdomains
cannot toss, once every app is on the apex.

## Admission requirements (per app)

1. **No untrusted active content on the origin.** User-generated or third-party HTML
   and scripts must render on a separate sandbox origin (planned
   `*.tuturuuu-usercontent.com`), or in an opaque-origin sandbox: `sandbox` without
   `allow-same-origin`, or a `Content-Security-Policy: sandbox` response header. They
   must never be same-origin documents. See [current exposures](#current-exposures).
2. **Strict CSP, enforced.** Use nonce-based `script-src 'nonce-…' 'strict-dynamic'`
   with no `unsafe-inline` or `unsafe-eval` for scripts. Set `object-src 'none'`,
   `base-uri 'none'` and `frame-ancestors 'none'` (or a path-specific `'self'` where
   framing is required). Report-only is for burn-in on the subdomain; the policy is
   enforced before the app is routed onto the apex in any mode.
3. **Trusted Types, enforced.** Burn in `require-trusted-types-for 'script'`
   report-only, then enforce it before the app is routed onto the apex. Rich-text
   editors (Mail's Tiptap) get a named policy.
4. **Path-scoped `connect-src`/`frame-src`.** Defense in depth: list
   `https://tuturuuu.com/<app>/` and `https://tuturuuu.com/api/` plus the app's
   external hosts, never `'self'`. Meet's CSP then forbids `fetch('/mail/api/...')`.
   CSP ignores the path after a redirect, so this is a speed bump, not a boundary.
5. **The shared session cookie is `HttpOnly`.** See
   [session cookies](#session-cookies).
6. **Permissions-Policy per document.** Only Meet documents enable `camera`,
   `microphone`, `display-capture` and `autoplay`. Every other app's documents
   send `camera=(), microphone=(), display-capture=()`, which blocks use even when
   the origin holds a grant. This does not stop the `window.open('/meet/...')` route,
   so that remains a residual risk (see the core trade).
7. **No storage, message or SW cross-talk.**
   * Prefix every `localStorage`/IndexedDB/Cache Storage key and `BroadcastChannel`
     name with the app slug.
   * `postMessage` handlers must not treat `event.origin === location.origin` as
     proof of a trusted sender. Use dedicated `MessageChannel` ports or explicit,
     validated message types.
   * Register no service worker, or one scoped to `/<app>/`.
8. **Security review.** An independent reviewer signs off on the app's CSP, cookie
   and content inventory at the exact head that gets routed.

## Session cookies

Decision (user, 2026-10-25): **one shared `tuturuuu.com` session, no per-app split.**
The Start bridge lane designs the implementation. This page records the
requirements it must meet.

* One session cookie on the apex is valid for every app. Web and every mounted app
  accept the same token. There are no per-app names, no `Path=/<app>` scoping and
  no per-app audience tokens on the apex.
* Web must stop treating the session cookie as a satellite marker. Today
  `createClient(request)` (`packages/supabase/src/next/server.ts`) returns an
  unauthenticated client when `tuturuuu_app_session` is present. Web must neither
  drop nor clear the shared session.
* Attributes: `HttpOnly`, `Secure`, `SameSite=Lax`, `Path=/`, host-only. Use
  `__Host-` once no subdomain needs it, so subdomains cannot toss or overwrite it.
  Refresh and rotation are single-writer: concurrent refreshes from two apps must
  not invalidate each other.
* Logout from any app ends the session for all of them. `Clear-Site-Data` on logout
  is acceptable for that reason.
* Mounted apps never write or clear per-app session cookies on the apex.
* There is no cross-host handoff. Users who already hold the root session stay
  signed in; only users without one sign in once on the apex at cutover (recorded
  decision). Subdomains keep their current cookies until they only redirect.
* The decision is conditional on a clean independent security review of refresh
  rotation, CSRF and the XSS blast radius below. Anything beyond that scope goes
  back to the user.

### What the shared session changes in the threat model

A per-app path-scoped cookie never protected against same-origin script; the core
trade above already holds. The shared session removes the remaining speed bump:
**an XSS in any mounted app acts with the full session for every app and Web's
dashboard**. It can call any app's API as the user and reach every app's data.
`HttpOnly` stops the token being read, but script on the origin does not need to
read it to use it.

That raises the admission bar. Before any app is routed onto the apex:

1. **Strict CSP is enforced, not report-only**: nonce-based scripts and no
   `unsafe-inline` or `unsafe-eval`. A report-only policy is not admission. This
   depends on the bridge supplying a per-request nonce to the router (`ssr.nonce`).
2. **Trusted Types is enforced** (`require-trusted-types-for 'script'`), with named
   policies only for audited sinks.
3. **No user-generated or third-party active content on the apex.** Email HTML,
   playground previews and uploads render on a separate sandbox origin, or in an
   opaque-origin sandbox with a regression guard. "Low risk because scripts are
   disabled" is no longer enough on its own.
4. **No same-origin `postMessage` or storage trust** between apps without explicit
   validated message types.
5. **Independent security review** of each app's CSP, sinks and content inventory at
   the exact routed head. An open critical or high finding holds the route.
6. **Session hardening:** short access-token lifetime with refresh, re-authentication
   (aal2) for sensitive actions, and CSP violation monitoring that can trigger a
   route rollback.

## Shared security headers

Each app Worker applies one portable header builder (planned
`@tuturuuu/utils/security-headers`, framework-free). Web applies the same baseline
through its Next config:

* `Strict-Transport-Security: max-age=63072000` (decide `includeSubDomains`/preload
  separately; it affects every subdomain).
* `X-Content-Type-Options: nosniff`.
* `Referrer-Policy: strict-origin-when-cross-origin`.
* `Cross-Origin-Opener-Policy: same-origin`. This isolates the app from cross-origin
  openers, not from same-origin peers.
* `Cross-Origin-Resource-Policy: same-origin` on API and private responses.
* `X-Frame-Options: DENY` plus `frame-ancestors`. This follows the existing
  `TUTURUUU_ANTI_FRAMING_HEADERS` and Mail `response-policy.ts` behavior.
* Per-app CSP and Permissions-Policy, as above. Private pages keep
  `Cache-Control: private, no-store` and `X-Robots-Tag`.

## Current exposures

| App | Surface | Today | Required before admission |
| - | - | - | - |
| Mail | Email HTML reader (`mail-message-preview.tsx`) | `srcDoc` iframe, `sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"`, no scripts. `allow-same-origin` serves keyboard handling. | Keep scripts impossible. Add a regression guard that fails if any Mail frame combines `allow-scripts` with `allow-same-origin`. Long-term, move to a usercontent origin plus `postMessage` for keys. |
| Mail | Connected-mail reader, signature and docx previews | Sandboxed `srcDoc`, CSP `default-src 'none'` in the frame | Same guard |
| Mail | Attachments | `sandbox; default-src 'none'; frame-ancestors 'self'` response CSP | Keep, and add the `/mail` prefix to the path regexes in `response-policy.ts` |
| Meet | Programming preview proxy (`/api/meet-call/[id]/programming/preview/...`) | Response CSP `sandbox allow-scripts allow-forms` (opaque origin), no cookies forwarded | Move to a usercontent origin, **or** fence every response on that prefix (including errors) with the sandbox CSP and a test |
| Web | Service worker at scope `/` | Controls every path | Bypass mounted prefixes (PR P3) |
| All | `localStorage` keys | Mostly app-prefixed (`tuturuuu-mail-*`, Meet ended-room prefix) | Audit and prefix the rest |

## Residual risk and its owner

The residual risk is an XSS in any mounted app acting with the shared session for
every app, including media permissions granted to Meet. The release authority
accepts it once per app at admission. Mitigations: enforced strict CSP and Trusted
Types (prevention), an `HttpOnly` shared session (token theft, not misuse),
path-scoped `connect-src` (casual
exfiltration) and per-document Permissions-Policy (silent media use). CSP violation
reports from every mounted app feed one monitored sink, and a new violation class
is triaged as a potential release hold.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.