# 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. Also built (hosted): a **tenant-key onboarding endpoint** — `op:'register-owner'`, gated by a platform-signed grant
> scoped to the app + owner-key id, create-once — so a tenant self-registers its owner key instead of the operator
> running the CLI. And **key rotation** (`op:'rotate-owner'`, authorized by the current key) — the render verifies each
> room against the specific key its marker names, so rotating doesn't break existing rooms, and a `revoke` cuts a
> compromised key. **The owner rule is now complete end-to-end.** 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.

## Only the app can mint its rooms (room-genesis)

There is a subtlety the policy alone doesn't cover: the signed policy is a **reusable, public artifact** — it's the same
document on every one of the app's rooms, so anyone could copy it off one room and stamp it on a room *they* create.
Verifying the policy proves "*governed by* app X's rule," not "*created under* X's authority." Left there, an attacker
could stand up a look-alike room with X's exact governance and sign-in and phish X's users.

So stamping a policy at creation also requires a **room-auth** — a signature *by X's owner key over this specific room*.
Only the app owner (its backend) holds the key, so only the app can mint a room stamped with its app; the room id is in
the signed core, so a room-auth for one room can't authorize another, and it's bound to the *resolved* owner key (the
single key on-prem, or the tenant's registered key hosted), so it's app-specific. Create refuses a stamped policy with
no valid room-auth. This is what turns "governed by X's rule" into **"a genuine room of X's."**

## 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.
- **Impersonate the app** by stamping its public policy on your own room? No — creation requires a room-auth signed by
  the app's owner key over that specific room (room-genesis, above), so only the app can mint a room stamped with its app.
- **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."**
