# The owner rule — the owner decides who may join, and at what tier

> **Status: built · opt-in · behavior-neutral — not yet enabled in production.** The full path below is implemented and
> tested end-to-end for **both** trust-root modes: **on-prem**, where the owner pins a single `OWNER_POLICY_KEY`, and
> **hosted multi-tenant**, where a **tenant registry** (`OWNER_REGISTRY=1`) resolves the trusted key *by the room's app*
> so each governed room is rooted in whichever registered tenant owns it (a policy for an unregistered app, or one signed
> by the wrong key, is refused). Also built: the create-time mandate, the render's integrity gate, tier-driven
> capabilities, client peer verification, and an operator CLI (`tools/owner-policy.mjs`) to mint keys, sign policies, and
> onboard tenants. It stays **inert until an owner opts in** — keys unset, rooms behave exactly as before, the AWS build
> is unchanged. Remaining productization: the *tenant-key-authed onboarding endpoint* (the registry write path — today
> it's the admin CLI, create-once) and key rotation. This is how an *app owner* gets authority over admission without
> breaking the content-blind model.

## The gap

A Witbitz room is a door whose key is the link. Whoever creates the room picks the lock, and the choice is sealed —
even the platform can't read it. Great for a private room between equals; a problem the moment there's an **owner**. If
a company runs an app on Witbitz, it has no way to say *"every room in my app is only for my people, and gold customers
get more than free ones."* A creator can leave the door open, and the owner can neither see nor override it. Admission
is the creator's private choice; there is no owner authority above it.

This is the design that adds one — turning **"the link is the authority"** into **"the owner is the authority,"**
*opt-in*, without giving up content-blindness.

## The rule: a signed App Policy

The owner writes one small document and signs it with a key only they hold:

```
AppPolicy {
  app:      "acme-portal"                       // which app this governs
  ownerKey: <owner's public key>                // the root of authority
  version:  3                                   // for rotation
  tiers: [
    { name:"open",   admit: anonymous,                                      caps:{ read },                       limits: low  }
    { name:"member", admit: { issuer:"acme.okta.com", any:true },           caps:{ read, chat },                 limits: mid  }
    { name:"gold",   admit: { issuer:"acme.okta.com", claim:{group:"gold"} },caps:{ read, chat, act, premium },  limits: high }
  ]
  roomTiers: { default:"member", admits:["open","member","gold"] }          // what a room may be created as
  notBefore, notAfter
}
→ signed by ownerKey
```

The signature is the whole trick: nobody without `ownerKey` can produce a valid policy, so nobody can forge a *weaker*
one. Admission stops being binary — it's a **ladder**. `open` is anonymous with read-only caps (a freemium/preview
tier); `gold` is a verified user carrying a specific entitlement claim, with full capabilities. One signed table spans
anon-public to premium-verified, and the flat "must be my user" rule is just its one-tier case.

## Where the owner's authority actually lives

Two anchors, because two different parties must verify the policy:

- **The room-creation credential** — hosted, the owner's **tenant key**; on-prem, the owner's own create endpoint. This
  is what makes a room *belong* to the owner: an owner-room can only be minted through the owner's credential.
- **A pinned copy of `ownerKey`** — baked into the owner's app and published at a well-known URL — so the *render* and
  *other members* can independently check that a room's policy was really signed by this owner.

## Bound to a room at birth

When a room is created through the owner's credential, `create` stamps two things onto it:

1. **The signed policy, sealed** in the room config — so the render can read and enforce it with the room key.
2. **A cleartext marker** on the public record — `{ app, ownerKeyId, policyHash }`, generalizing today's `gated` flag —
   so the content-blind platform and the keyless read paths know "this room is owner-gated" without reading anything.

And the owner's create path **refuses to mint a room that doesn't carry the policy**. The room is *born* gated; there is
no "open" escape hatch under the owner's app.

## How a member's tier is decided — and why they can't self-promote

Tier comes only from a **signed** source, never the client's word:

- **IdP claims** — the owner's login token already carries groups/roles. The policy maps claim → tier, so the *owner's
  directory* is the source of truth for who is gold. Sign in, and the token's claim picks the tier.
- **A signed grant** — a room admin can promote a specific member for a specific room (the existing signed-capability
  grant, see [Delegated authority](https://docs.witbitz.chat/docs/delegated-authority.md)), overriding upward.
- **The room's tier** — a room is created *as* a tier; a member's effective tier is their entitlement, bounded by what
  the room admits (the room is the ceiling).

A tampered client can't upgrade itself, because it can't forge the IdP claim or the grant — and the **render**, not the
client, resolves the tier and enforces it.

## How it's enforced — four layers, no single point to bypass

| When | Check | Where it runs |
|---|---|---|
| **Birth** | create refuses any owner-room without the signed policy + marker | the owner's create path (tenant-keyed / on-prem) |
| **Writes** (every turn) | the render opens the sealed policy, **verifies its signature against the pinned `ownerKey`**, resolves the member's tier, and gates each action with the capability gate. Policy absent or signature invalid → refuse. | the certified render (already decrypts per turn to check admission) |
| **Blind reads** | the cleartext marker makes keyless reads refuse; keyed reads pass the same identity gate | the content-blind platform |
| **Join** | the member's app reads the room policy and **verifies it against its own pinned `ownerKey`** — and won't join a room whose policy is missing or unrecognized | every member's app (peer check) |

The render already does the per-turn identity gate and the capability gate; the **new** move is that the policy now
comes from the *owner-signed artifact* and the render **refuses to run if it doesn't verify** — instead of trusting the
creator's choice. The boolean tier caps drop straight into the existing gate; the one genuinely new enforcement is
per-tier **limits** (rate / token / tool budget), and the platform already meters sessions and tools — so a gold tier
is just a higher metered ceiling. Which means the ladder doubles as the **owner's pricing surface**: admission,
capability, and monetization collapse into one signed table.

## Why nobody can strip or weaken it

- **Forge a weaker rule?** No — it must be signed by `ownerKey`.
- **Omit it at creation?** No — the owner's create path won't mint the room, and the render refuses a room with no valid policy.
- **Delete the policy or marker later?** The render refuses a room whose policy doesn't verify; the marker guards the blind paths; peers reject unrecognized rooms.
- **The only escape** is creating a room *outside* the owner's app — which isn't "a room in the owner's app," and the owner's users' apps won't join it. So *"every room in my app requires my login"* holds exactly.

## Registration — Witbitz federates; it doesn't run the directory

A "registered user" is a record in the **owner's** identity provider, not a Witbitz account. Three distinct steps, and
only one is a fresh signup:

1. **Owner registration (once, the trust root).** The owner registers the app: its `ownerKey`, its IdP (issuer + JWKS,
   the [`SPACE_OIDC_ISSUERS`](https://docs.witbitz.chat/docs/on-prem.md) entry), and its claim → tier map. Hosted, this
   is tenant onboarding proven with the tenant key; on-prem, it's config. After it, the render and peers know *"tenant
   Acme's authoritative ownerKey is X."*
2. **User registration (per user, in the owner's directory).** However the owner's IdP already does it — enterprise
   SSO / SCIM provisioning, self-signup in the owner's flow, or admin invite. The tier is a group the owner assigns, so
   a user can't self-register as gold. If the owner has no directory, they can use a managed auth provider or Witbitz's
   email-code issuer *as* the directory — but the owner still owns the "who is gold" data.
3. **Device enrollment (per device).** On first use, the app mints a signing key and binds it to the identity at
   sign-in. One identity, many devices, each enrolled independently; losing a device just re-enrolls a new key against
   the same registration.

So **joining a room is authentication, not registration** — the room federates a sign-in against a registration that
already exists and reads the tier from the signed token:

```
register once (owner's directory) → owner assigns a tier (a group) → open a room link
  → app forces sign-in via the owner's IdP → token {identity + tier claim} bound to the device key
  → render admits at that tier
```

Revocation is symmetric: remove the user from the directory (or the `gold` group) and their *next* sign-in fails or
drops a tier — enforced on the very next turn.

## Orthogonal to attestation

This is the **identity + capability** axis. Whether the client is *trustworthy code* is a separate axis —
[the attested client](https://docs.witbitz.chat/docs/the-attested-client.md). They compose: a tier's `admit` can
*additionally* require an attested client (e.g. "gold requires the attested app"), but identity-tiers work on the plain
web with no attestation in sight. Keep them separate — one is about the person, the other about the code.

## The honest trade

Requiring registration deliberately reintroduces a **central authority** — the owner's directory — into a model that is
otherwise account-free and link-based. That's not a regression; it's exactly the control an owner asks for, and it is
**opt-in per app/room**: governed, tiered rooms federate to a directory; open link-rooms don't. The owner chooses where
on the spectrum each room sits. The platform stays content-blind throughout — the policy is enforced in the certified
render and by peers, never by a platform that reads the room.

## What's reused, and what's new

- **Reused:** the sealed `admission`/`allow`/`revoked` config + the render's per-turn gate, the `gated` cleartext
  marker, pluggable OIDC issuers, tenant keys, the signed-grant **capability gate**, and session/tool metering.
- **New:** (1) the signed `AppPolicy` artifact + a pinned `ownerKey`; (2) the create-time *mandate* through the owner
  credential; (3) the render's *signature check* + refuse-if-invalid; (4) peer verification in the app; (5) the tier
  table + claim → tier resolution + per-tier limits wired to metering.

One line: **the owner's signed policy replaces the creator's free choice — stamped on every room at birth through a
credential only the owner holds, resolving each federated sign-in to a tier, and enforced in three independent places
(render, blind paths, peers) — so a room's authority moves from "whoever holds the link" to "whoever holds the owner
key."**
