# REQUEST → BW Auth: the one-prompt app standard

**From:** the Scout build session (the first full-suite integration of the
accounts kit). **To:** the id-auth/BW Auth build session. **Companion file:**
`notes/bw-auth-feedback.md` (T1–T11, G1–G5) — this document turns that feedback
plus rian's next goal into a buildable requirements prompt.

**Rian's goal, verbatim in spirit:** *"Spin up a new app with a full user
system — me as super admin, levels, accounts, grants, View As, hub reporting —
with ONE prompt, specifying only what deviates from the norm (e.g. 'this app
has no instances'). Minimal work by Claude to get the shell."*

**Definition of done:** a fresh app reaches Scout's entire auth surface —
sign-in, levels-as-data with an editor, per-instance + app-wide assignment,
View As, invites, resets, matrix, hub visibility, the conformance tests green —
from **one owner command plus one Claude prompt**, with the app-specific work
limited to naming the app, picking deviations, and building its actual product
features. Scout took three milestones and ~15 hand-built files to get there;
the target is near zero.

---

## 0. The target workflow (design everything backwards from this)

```
rian:   srv-gw create-project --name widgets --type custom --domain widgets.bowden.works
        (or the scaffolder does this)
rian:   one prompt to Claude: "Stand up widgets.bowden.works as a bw-app.
        No instances. Otherwise all defaults."
claude: runs the scaffolder, fills the app config (name, noun, has_instances,
        seed levels), asks rian to run the ONE owner-gated command
        (app-client-register --secret-out), deploys, runs the conformance
        suite, hands over a live app where rian signs in as super admin and
        the account menu already contains Profile / Admin / View As.
```

Everything below exists to make that transcript real. The measure of success is
what Claude does NOT have to write.

## 1. Read first

1. `notes/bw-auth-feedback.md` in the Scout workspace — every tension and gap
   referenced below (T*/G*) is specified there with the file that hit it.
2. Scout's app-side inventions, which this request asks you to ABSORB into the
   kit so Scout can delete them on its next re-vendor:
   - `main/app/services/accounts_store.py` — the SQLAlchemy store (§2.1)
   - `main/app/services/levels.py` — `assignable_on`, `project_can_any`,
     `access_matrix`, the permission catalog plumbing (§2.3)
   - `main/app/services/view_as.py` — permission-weighted `rank_of` (§2.4)
   - `main/app/routers/{levels_admin,auth_admin,view_as}.py` + the
     invite/reset halves of `accounts.py` — the standard admin API (§2.2)
   - `main/app/constants.py::PERMISSION_CATALOG` — permission metadata (T2)
   - `main/frontend/src/admin/*` + `components/ViewAs*` — the admin UI (§2.5)
   - `main/tests/{test_levels,test_view_as,test_full_suite}.py` — the
     invariants for the conformance pack (§6)

These are listed as provenance for the asks, not as a finished design — where
kit-side design contradicts them, the kit wins and Scout will conform.

## 2. The kit: what `/srv/system/id-auth/app-auth/` should ship

### 2.1 `bw_store_sqlalchemy.py` — stop hand-writing the store

The storage-pluggable contract is the kit's best feature (feedback: Works #1);
the cost is that every SQLAlchemy app re-writes the same 16 methods and the
same four tables. Ship a ready adapter:

- Factory taking a session factory + optional table-name overrides; ships its
  own SQLAlchemy models (levels / members / instances / grants) AND accepts
  column mappings onto existing tables for apps like Scout that already have
  them.
- An Alembic migration TEMPLATE (documented, copy-in) for the four tables,
  carrying the two hard-won warnings as comments: never auto-write a grant
  below an all-instances holder's level (T5), and seed levels append-only.
- The `active`-member contract made explicit (feedback B1): the contract doc
  states `get_member` → None for inactive members, and the reference adapters
  implement it.

### 2.2 `bw_admin_api.py` — the standard admin API as a mountable router pack

Scout hand-built ~14 endpoints that will be identical in every app. Ship them
as a framework-adapter pack (FastAPI first; the Flask example can keep its
server-rendered page):

- **Surface:** levels CRUD (+ permission catalog + `assignable_by_me` +
  `can_edit`), accounts list/create/invite/level/active/send-reset, per-instance
  member add/level/remove, view-as targets/start/stop, access matrix, BW
  status/sync, cross-app access lookup, self send-reset. Error shape:
  `{error_code, summary, details}` with the kit's codes verbatim.
- **The `/api/me` capability contract, standardized** (this is the load-bearing
  UI seam Scout proved): server-decided booleans (`is_owner`, `can_*`),
  `assignable_levels`, the view-as block (`impersonating`, `viewing_as`,
  `viewing_as_label`, `real_user`, `view_as_mode`, `can_write`,
  `can_view_as_others`), `account_url`. Document it as THE shape; the frontend
  pack consumes exactly this. UIs must never switch on level names — the
  contract is what makes that rule cheap to follow.
- **Hooks, not forks:** the app injects its identity dependency, its instance
  loader (returns 404-vs-403 per the asymmetry Scout encodes), and optional
  per-route extra guards. Composite flows (invite = create + level + grant)
  route level changes through the ONE guarded path — the side-door class from
  T8, closed structurally in the pack rather than re-derived per app.
- **Enforced-permission registry (T3/T11):** the pack registers every
  permission it actually checks; `known_permissions` marks `enforced` from that
  registry, not from hand-kept claims. Apps register their own app-specific
  permissions the same way; anything attached to a level but never registered
  renders as "custom — not enforced yet" honestly and automatically.

### 2.3 `bw_accounts.py` upgrades (all additive)

- `PERMISSION_INFO`: label + one-line description + scope (`app`|`instance`)
  for the standard vocabulary (T1, T2). Scout's catalog is a starting draft.
- `assignable_by(username, instance_id=None)` — per-instance assignability,
  symmetric with `effective_level` (T9; this one broke a real flow).
- `access_matrix()` next to `sync_reports()` (G3) — one derivation of
  resolution order, not one per app.
- Optional `audit=fn(action, actor, target, detail)` on every mutation (G5),
  symmetric with bw_view_as.
- ACCOUNTS.md: the owner-synthesis contract stated loudly (T4 — `level_def` of
  the owner's synthesized level is None; check `is_owner` first), the T5
  migration warning, the T8 composite-flow rule.
- Standard permission name `accounts.view_as` in the vocabulary (G1).

### 2.4 `bw_view_as.py` upgrades

- Ship a default **permission-weighted `rank_of`** (T10) built on
  `PERMISSION_INFO` weights, so dynamic levels rank sensibly out of the box;
  keep the hook for apps that want their own. VIEW-AS.md gets the warning that
  name-keyed ranks and editable levels are incompatible (Scout hit this as a
  real refused-everything bug).
- The SPA field block already exists (`me_fields`); document the
  `mode`→`view_as_mode` rename convention or rename it in the kit so no app
  collides.

### 2.5 The frontend pack — vendorable React source (the biggest lever)

Scout's admin surface was two subagent-days of UI work that the next app would
repeat. Ship `app-auth/react-admin/` as **source to vendor** (full independent
copy per app — server rule: no shared runtime deps between apps), tokens-based
so it inherits each app's theme:

- **Panels:** People, Levels (editor with described permission checkboxes,
  custom-string chips, assignability), Access matrix, Auth status/sync/lookup.
- **View As:** banner (un-hideable, bottom-fixed) + picker dialog (stop-first,
  act-mode radio gated on `is_owner`).
- **Sign-in screen** with the forgot-password affordance (until G2 lands
  centrally, then it becomes a link).
- **`UserPicker`** — the typeahead rian asked for: debounced search against
  §3.1's directory endpoint; renders username + name + email; falls back to
  exact-username validation when the directory is unavailable. Used everywhere
  a username is entered (add person, invite, view-as target).
- **`AdminTable`** — the standard table primitive: client-side text search,
  per-column sort, simple filters (level, active, all-instances). The matrix
  and People list ship using it. Dependency-free, token-styled. This answers
  "search/sort/filter as standard" once, for every app.
- **`AccountMenu` + the IA standard** (rian's explicit ask): a top-right menu
  with a documented structure — **Profile** (in-app account page: §3.2),
  **Admin** (the panels, when permitted), **View as…** (when permitted),
  **Sign out**. Written up in a short `UI-STANDARD.md` so every bw-app presents
  identity the same way; the pack implements it, apps restyle via tokens.
- Consumes exactly the §2.2 `/api/me` contract and the generated OpenAPI types;
  zero level-name switches anywhere in the pack.

## 3. Gateway / central asks (new capability, not kit code)

### 3.1 User directory search — `POST /app/search-users` (NEW)

Typeahead needs a directory; today an app can only validate an exact username
(`userinfo` 404s otherwise), so "start typing and autopopulate" is impossible.

- Request: `{client_id, client_secret, q, limit}`; matches username OR email OR
  name, prefix-weighted; returns `[{username, email, first, last, class}]`.
- **Privacy decision required (flag to rian):** which accounts may an app see?
  Recommended scope: accounts already reported to THIS app, plus accounts of
  class `external` created by/for this app's invites, plus staff — NOT the
  whole BW directory by default, with a per-client `directory_scope` flag
  (`own|all`) the owner can widen for first-party apps. Rate-limited; audit-
  logged like db-query.

### 3.2 In-app credential management (rian's explicit ask)

Rian wants password change INSIDE the app's own profile page, not a bounce to
auth.bowden.works. Today the principle is "the password never transits your
app" (`profile_html` says so verbatim), so this is a deliberate policy change,
not a small endpoint. Two designs — **decide with rian before building:**

- **(a) Trusted-transit endpoints (recommended for this fleet):**
  `POST /app/change-password {client_id, client_secret, username,
  current_password, new_password}` — gateway verifies the current password,
  enforces policy + rate limits, never returns the password, app never stores
  or logs it. Justification: every bw-app is first-party, server-to-server on
  this box, TLS; the UX win is exactly what rian asked for. Guardrails: only
  for the session's own username as attested by the app; per-client toggle
  (`allow_password_transit`) the owner sets; the amber-2FA class of clients
  could be excluded.
- **(b) No-transit embedded flow:** a chromeless central page
  (`/account/embed?client_id=…`) themed neutral, opened in-app (dialog/iframe
  with postMessage completion) so the password still never touches the app.
  Weaker fit for "the app has control of the UI," stronger principle.

Whichever wins, also ship as JSON (feedback G4): `profile_info(username)` —
`{account_url, has_password, google_linked, class}` + `user_access` — so the
in-app Profile page renders natively: identity, change password (per the
decision), "email me a reset link" (exists), linked sign-ins (Google link
STATUS in-app; the link ceremony itself may stay a central redirect — it is a
Google OAuth dance regardless), and "your other BW apps" from `user_access`.

### 3.3 Central forgot-password on the BW login page (G2 — promote to build)

The highest-user-value item in the feedback file. Once `auth.bowden.works`'s
own login page has "Forgot password?", every app's stopgap (Scout ships one)
reduces to a link. Scout will delete its `/auth/forgot` the release after.

### 3.4 Resolve the `send_reset` scoping contradiction (T6 — decide, then doc)

Handoff says sign-in-gated (+`app-link-user` escape hatch); docstring says
report-gated. Pick one, update both documents, and state the anonymous-abuse
posture (constant response, rate limits) either way.

## 4. The scaffolder — where "one prompt" becomes real

Extend the workspace scaffolder or add `app-auth/new-bw-app.sh` (script style:
idempotent, never overwrites, the script IS the definition):

1. Creates/uses the project (gateway), applies the v8 workspace shape.
2. Instantiates the app shell: FastAPI + React per the react.md stack, with the
   vendored kit files, the §2.2 router pack mounted, the §2.5 frontend pack,
   the §2.1 store + migration template, Pattern B auth routes, default-deny
   middleware, version/changelog plumbing, and the conformance test pack (§6).
3. Writes `bw-app.toml` (or equivalent) — the deviation surface, which IS the
   one-prompt vocabulary:
   ```toml
   app_name    = "widgets"
   owner       = "rian"
   has_instances = true          # false drops instances/grants/matrix columns
   instance_noun = "project"     # "client", "workspace", "site"…
   seed_levels = "default"       # admin/lead/reviewer, or inline definitions
   view_as     = true
   invites     = true
   hub_reporting = true
   ```
4. Prints the two things Claude cannot do alone: the owner's
   `app-client-register --secret-out` command, and the DNS/domain step if new.
5. `--check` mode re-runs the conformance suite against a live app.

CHECKLIST.md gains a "bw-app quickstart" that IS the one-prompt script: what
rian types, what Claude does, the acceptance run.

## 5. Documentation set (delta)

- **QUICKSTART-BW-APP.md** — the §0 transcript, exact.
- **UI-STANDARD.md** — the account-menu IA, the capability-contract rule
  (never switch on level names), the table/search expectations, View As
  banner/picker behavior.
- ACCOUNTS.md — §2.3's additions (owner contract, T5, T8, enforced-registry).
- VIEW-AS.md — dynamic-levels rank warning + default rank_of.
- PATTERN-B.md — unchanged protocol; add the `/api/me` capability contract as
  an appendix (it is the SPA's half of the pattern now).
- Feedback loop: keep `notes/bw-auth-feedback.md`-style files as the standard
  channel — this round proved it works.

## 6. The conformance test pack

Scout's 92-test suite encodes the invariants; ship the generic ~40 as
`app-auth/tests/` that the scaffolder copies in, parameterized by
`bw-app.toml`:

- default-deny (anonymous 401 everywhere but the allowlist)
- 404-vs-403 asymmetry for instances; capability flags never lie
- resolution order (grant → all_instances → none); owner short-circuit (T4)
- assignability app-wide AND per-instance (T9); no-self-change; owner
  immutability; admin-can't-mint-admin
- view-as: policy, rank vs custom levels (T10), fail-closed auto-stop,
  read-only write block, act-mode gating, report_impersonation calls
- invite/reset gating incl. instance-scoped; the T8 side-door stays closed
- forgot-password constant response
- deactivation = immediate non-membership (B1 contract)
- all outbound BW calls stubbed through the one `_post_json` funnel + recorded
  (the pattern that let Scout's suite assert emails WOULD have been sent
  without ever sending one)

An app passing this pack = "full user system present," mechanically.

## 7. Decisions needed from rian (do these first; everything else is buildable)

1. **Password transit (§3.2 a-vs-b).** Scout's recommendation: (a) with the
   per-client toggle and guardrails. This is the one genuine security-posture
   change in the whole request — it deserves his explicit yes.
2. **Directory scope (§3.1).** Recommended default `own` (reported + invited +
   staff), per-client widen to `all`.
3. **Default seed levels** for new apps: Scout's admin/lead/reviewer, the kit's
   admin/manager/team/collaborator templates, or per-app at scaffold time.
4. Confirm G2 (central forgot-password) gets built this round — it obsoletes
   per-app stopgaps and answers half of "account stuff without leaving the app."

## 8. Acceptance (run this before calling it done)

Scaffold a throwaway app (`has_instances = false` — the deviation Scout can't
test), register it, deploy it, and confirm: owner signs in and sees the full
account menu; creates a level; invites a user through the typeahead picker (to
an owner-controlled inbox); the matrix (userlist form) searches and sorts; View
As round-trips with the banner; the central hub shows the app, its users, its
levels, and the live impersonation row; the conformance pack passes untouched.
Then re-vendor into Scout and delete Scout's local `accounts_store` /
`assignable_on` / rank map / admin routers where the kit now provides them —
Scout is the proof that absorption worked (`srv-gw deploy --project scout
--build` + its 92-test suite green stays the regression bar).

## 9. Non-goals / compatibility

- No breaking changes to the Pattern B protocol or the store contract —
  everything here is additive; Scout must keep working un-modified until it
  chooses to re-vendor.
- Central stays display-only for app access (D24 stands; no write-back).
- No shared runtime dependencies between apps — the frontend pack is vendored
  source, never an npm package apps import at build time from a shared path.
- The kit stays framework-agnostic at its core; the router/frontend packs are
  adapters around it, not replacements for it.
