Skip to content

Commit 65b7cec

Browse files
rafiki270claude
andcommitted
docs: apply loop 10 review fixes
Token/JWT: - api-changes-rebac.md: add APPLE to VerificationMethod enum and applicableVerificationMethods() - roles-and-acl.md: fix token refresh — orgs re-resolved from DB, flags NOT re-resolved (frozen at login) - roles-and-acl.md: fix "each team (and optionally org)" → team-scoped only; no org-level custom role set - roles-and-acl.md: remove "per team/org" phrasing from UOA responsibilities list SCIM: - roles-and-acl.md: add userName (email) change behavior — rejected with HTTP 400 (immutable) - roles-and-acl.md: add POST /scim/v2/Bulk — not supported, returns HTTP 405 - roles-and-acl.md: note PATCH /Users update user attributes (with email change ref) Feature flags: - feature-flags.md: clarify role matrix UI "Add Role" delegates to team role endpoints - feature-flags.md: add PUT overrides with empty flags:{} returns HTTP 400 - apps.md: clarify pollIntervalSeconds applies to both startup and flag query polls - apps.md: add DELETE App — SDK must re-validate on next launch even within TTL Architecture: - architecture-api.md: fix flag route paths to include /org/:orgId prefix Brief supersession notes: - brief.md §24.3: add note that domain-scoped org model is superseded by api-changes-rebac.md §1 - brief.md §24.5: add lead→admin supersession note for personal team creation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f2b35a6 commit 65b7cec

6 files changed

Lines changed: 23 additions & 15 deletions

File tree

Docs/Auth/architecture-api.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ For the full product spec, see [brief.md](./brief.md). For tech stack, see [tech
2828
/apps
2929
apps.ts — CRUD for /org/:orgId/apps[/:appId]
3030
killswitches.ts — CRUD for /org/:orgId/apps/:appId/killswitches[/:id]
31-
flags.ts — Flag definitions: GET/POST/PATCH/DELETE /apps/:appId/flags/definitions[/:flagKey]
32-
flag-matrix.ts — GET/PATCH /apps/:appId/flags/matrix[/:roleName/:flagKey]
33-
flag-overrides.ts — GET/PUT/DELETE /apps/:appId/flags/overrides/:userId[/:flagKey]
34-
flag-query.ts — GET /apps/:appId/flags (resolved flag map for a user)
31+
flags.ts — Flag definitions: GET/POST/PATCH/DELETE /org/:orgId/apps/:appId/flags/definitions[/:flagKey]
32+
flag-matrix.ts — GET/PATCH /org/:orgId/apps/:appId/flags/matrix[/:roleName/:flagKey]
33+
flag-overrides.ts — GET/PUT/DELETE /org/:orgId/apps/:appId/flags/overrides/:userId[/:flagKey]
34+
flag-query.ts — GET /apps/:appId/flags (resolved flag map for a user; domain-hash auth, no orgId in path)
3535
startup.ts — GET /apps/startup (combined kill switch + flags; public)
3636
killswitch-check.ts — GET /killswitch/check (standalone kill switch query; public)
3737
index.ts — Route registration for /apps and /killswitch

Docs/Requirements/apps.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Request body for `POST /org/:orgId/apps`:
6060
}
6161
```
6262

63-
`DELETE` is destructive — cascades to all kill switch entries and flag definitions for the App. Requires confirmation (`?confirm=true` query param).
63+
`DELETE` is destructive — cascades to all kill switch entries and flag definitions for the App. Requires confirmation (`?confirm=true` query param). **SDK cache behavior after App deletion:** SDKs holding a cached startup response must re-validate on next app launch or foreground resume, even if the cache TTL has not expired. After deletion, the `/apps/startup` endpoint returns an empty response (same as unknown `appIdentifier`) — the SDK should treat this gracefully and not block the app.
6464

6565
An App belongs to an **org**, not a team. Teams manage who has access; Apps define what those people are using.
6666

@@ -396,6 +396,6 @@ Every SDK must:
396396

397397
2. **Web app versioning****decided: use `semver` scheme with `versionName` only.** Web apps pass their semver tag (e.g. `1.5.0`) or deploy timestamp (as a date scheme, e.g. `2024.01.15`) as `versionName`. `buildNumber` is optional for web. The `versionScheme` field on the kill switch entry specifies which scheme to use.
398398

399-
3. **Flag sync in token vs query / polling interval****decided: SDK polls on foreground resume, minimum 60 seconds between checks, default 5 minutes.** This is configurable per App (see `pollIntervalSeconds` field — to be added to App data model as an optional integer, default 300, minimum 60). Token-embedded flags are login-time snapshots; mid-session changes require poll.
399+
3. **Flag sync in token vs query / polling interval****decided: SDK polls on foreground resume, minimum 60 seconds between checks, default 5 minutes.** This is configurable per App via `pollIntervalSeconds` (default 300, minimum 60, maximum 3600). Token-embedded flags are login-time snapshots; mid-session changes require poll. The `pollIntervalSeconds` value applies to both the `/apps/startup` endpoint poll **and** any direct `/apps/:appId/flags` query polls. SDKs must not poll either endpoint more frequently than `pollIntervalSeconds` (minimum 60 seconds between calls).
400400

401401
4. **Multiple matching kill switches****decided: highest `priority` integer wins.** If two entries have identical priority, the one with the earlier `createdAt` wins (first created takes precedence). This must be deterministic — no random tiebreaking. The `priority` field is an integer from 0 to 1000 (default 0). Entries with the same priority and `createdAt` are processed in ascending `id` order as a final tiebreaker.

Docs/Requirements/feature-flags.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,10 @@ New section in the sidebar, scoped to the selected App:
153153

154154
- Grid view: rows = flags, columns = roles
155155
- Each cell = toggle (on/off)
156-
- "Add Role" button — adds a column
156+
- "Add Role" button — adds a column by creating a new custom role on the selected team via the team role endpoints (`POST /org/:orgId/teams/:teamId/roles`). The matrix column appears once the role exists on at least one team in the org.
157+
- "Rename Role" / "Delete Role" / "Set Default" — these operations are team role management, delegated to `PATCH/DELETE/PUT /org/:orgId/teams/:teamId/roles/:name[/default]`. The matrix API itself only manages cell values.
157158
- "Add Flag" button — adds a row (or reuses a flag defined in the flags section)
158-
- Default role indicator (tick icon on column header, clickable to reassign)
159+
- Default role indicator (tick icon on column header, clickable to reassign via team role endpoint)
159160

160161
### User override UI
161162

@@ -201,7 +202,7 @@ Roles are derived from the team custom role union (see role matrix section). Add
201202

202203
```
203204
GET /apps/:appId/flags/overrides/:userId — get all overrides for a user (resolved state + source)
204-
PUT /apps/:appId/flags/overrides/:userId — set one or more overrides (body: { flags: { [key]: boolean } })
205+
PUT /apps/:appId/flags/overrides/:userId — set one or more overrides (body: { flags: { [key]: boolean } }). Empty `flags: {}` returns HTTP 400 (nothing to set; use DELETE to clear overrides).
205206
DELETE /apps/:appId/flags/overrides/:userId/:flagKey — remove a specific override
206207
DELETE /apps/:appId/flags/overrides/:userId — remove all overrides for a user
207208
```

Docs/Requirements/roles-and-acl.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ These are roles that a developer or org defines for their own product. UOA store
2727
- A user's UOA role and their custom role are completely orthogonal
2828

2929
UOA's only responsibilities for custom roles:
30-
- Store the role definitions per team/org, including which is the default
31-
- Validate that a role assigned to a user exists in the team/org's defined list
30+
- Store the role definitions per team, including which is the default
31+
- Validate that a role assigned to a user exists in the team's defined list
3232
- Return the role label(s) in the access token and via API
3333
- Enforce nothing beyond that
3434

@@ -69,7 +69,7 @@ Teams are the primary registration unit. Not every setup needs a full enterprise
6969

7070
## Custom role definitions
7171

72-
Each team (and optionally org) defines its own role names. One role is marked as the default.
72+
Each team defines its own role names. One role is marked as the default. Custom roles are team-scoped — there is no independently-defined org-level custom role set. The `org.customRole` field in the token is a convenience field derived from the user's primary team (see Token output section below).
7373

7474
```json
7575
{
@@ -152,7 +152,7 @@ Note: `flags` is present only when `feature_flags_enabled = true` on the App ass
152152

153153
**`orgs` when user has zero org memberships:** When org features are enabled but the user belongs to no org, `orgs` is an empty array (`"orgs": []`), not absent. The field is always present when `org_features.enabled = true`.
154154

155-
**Token refresh behavior:** When a refresh token is used to issue a new access token, `orgs[]` claims are **re-resolved from the current database state** at that moment — they are not cached from the original login. Flag values embedded in the token are also re-resolved at refresh time. Refresh tokens themselves carry no org or flag data.
155+
**Token refresh behavior:** When a refresh token is used to issue a new access token, `orgs[]` claims are **re-resolved from the current database state** at that moment — they are not cached from the original login. Refresh tokens themselves carry no org data. **Flag values are NOT re-resolved on refresh** — they reflect the state at login time (see `feature-flags.md §Resolved decisions #3`). For real-time flag changes mid-session, the consuming app calls the `/apps/:appId/flags` query endpoint directly.
156156

157157
**`org.uoaRole` derivation for multi-team users:** The org-level `uoaRole` is the highest UOA system role the user holds across all teams in that org (accounting for inheritance). If a user is `owner` on one team and `admin` on another, their org-level `uoaRole` is `owner`. If the user has no `owner` or `admin` role on any team (and no org-level role assignment), `uoaRole` is omitted.
158158

@@ -241,6 +241,7 @@ GET /scim/v2/Groups/:id — read a single group/team with its members
241241
POST /scim/v2/Groups — create team via IdP
242242
PATCH /scim/v2/Groups/:id — add/remove members, rename team (RFC 7644 Operations[] format)
243243
DELETE /scim/v2/Groups/:id — delete team and sever ScimGroupMapping
244+
POST /scim/v2/Bulk — not supported; returns HTTP 405
244245
```
245246

246247
**SCIM bearer token:** An opaque UUID token issued per org via the admin panel. It has no expiry by default (long-lived). A single org may have multiple active tokens (for rolling rotation or multiple IdP integrations). Tokens are stored hashed; plain value shown only at creation. Revoked via admin panel (`DELETE /internal/admin/orgs/:orgId/scim-tokens/:tokenId`). Scoped to a single org — cannot be used across orgs.
@@ -254,6 +255,8 @@ DELETE /scim/v2/Groups/:id — delete team and sever ScimGroupMapping
254255
- If a user with the same email already exists in UOA, the SCIM-provisioned user is linked to the existing account (matched by email)
255256
- `externalId` (IdP-provided) is stored on the UOA user record for stable re-linking on reprovision
256257

258+
**Email (userName) changes via PATCH:** `userName` maps to UOA email (the canonical user identifier). **Email changes via SCIM PATCH are rejected with HTTP 400** — email is immutable after account creation. If an IdP sends a new `userName` in a PATCH body, UOA returns `{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], "status": 400, "detail": "userName is immutable" }`. The IdP should provision a new user with the new email and deprovision the old one separately.
259+
257260
**Custom role assignment:** The custom role assigned to SCIM-provisioned members defaults to the team's default custom role. If the IdP sends a role via SCIM enterprise extension (`urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization` or a custom schema attribute `uoa:customRole`), that role is validated against the team's defined custom roles and assigned if valid. If the role is unknown, provisioning proceeds with the default role (not rejected).
258261

259262
**Deprovisioning behavior (`DELETE /scim/v2/Users/:id`):**

Docs/Research/api-changes-rebac.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@ model OrgEmailDomainRule {
3939
}
4040
4141
enum VerificationMethod {
42-
ANY // any verified identity (email link, Google, GitHub, Microsoft, etc.)
42+
ANY // any verified identity (email link, Google, GitHub, Microsoft, Apple, etc.)
4343
EMAIL // email link only
4444
GOOGLE
4545
GITHUB
4646
MICROSOFT // Microsoft Entra ID / Azure AD OIDC
47+
APPLE // Apple Sign In
4748
}
4849
```
4950

@@ -232,6 +233,7 @@ for (const rule of matchingRules) {
232233
- Google login → matches `ANY`, `GOOGLE`
233234
- GitHub login → matches `ANY`, `GITHUB`
234235
- Microsoft login → matches `ANY`, `MICROSOFT`
236+
- Apple login → matches `ANY`, `APPLE`
235237
- Email verified → matches `ANY`, `EMAIL`
236238

237239
### `GET /user/me` and access token payload

Docs/brief.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,8 @@ groupMembers GroupMember[]
809809

810810
### 24.3 Organisation
811811

812+
> **Note:** The "one org per domain" constraint and domain-based org scoping below predates the ReBAC model. `api-changes-rebac.md §1` removes `Organisation.domain` — orgs are no longer tied to a single client domain. Auto-enrolment via `OrgEmailDomainRule` replaces domain-scoped org membership. Implementers should follow `api-changes-rebac.md §1–3` for the current model.
813+
812814
* An organisation belongs to a **domain** and is the top-level tenant concept.
813815
* A domain can have **multiple organisations**.
814816
* A user belongs to **exactly one organisation** per domain (with global `user_scope`, the same person could belong to different orgs on different domains).
@@ -886,7 +888,7 @@ Organisation slugs are derived from the `name` field:
886888
* Every org member must belong to at least one team.
887889
* On org membership add, user is auto-added to the default team.
888890
* If `org_features.user_needs_team = true`, successful auth must self-heal users with no team membership:
889-
* If the user already belongs to an org on the domain but has zero teams, create a personal team named `"{user name}'s team"` and add them as `lead`.
891+
* If the user already belongs to an org on the domain but has zero teams, create a personal team named `"{user name}'s team"` and add them as `lead`. **⚠ SUPERSEDED:** `lead` is replaced by `admin` per `api-changes-rebac.md §1`.
890892
* If the user does not belong to any org on the domain, create a new personal org for them, create a default personal team named `"{user name}'s team"`, and place them there.
891893

892894
#### Default Team

0 commit comments

Comments
 (0)