Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

console: after a refused switch to an organization the user does not belong to, the reloaded header still names the previous organization while the server reports NO_ACTIVE_ORGANIZATION #12698

Closed

Description

@objectstack-fleet

Filing gate ① — a user-visible state mismatch, measured once live. reach: on the stock showcase (objectstack 2034975a, console at .objectui-sha daeb77fc), a forged set-active to an organization the user is not a member of is refused; after a reload the console header still names the previous organization, while the server reports NO_ACTIVE_ORGANIZATION for the session. The header claims a workspace the server does not have active, so org-scoped screens act on a context the user cannot see is gone. The refusal itself is correct; nothing is exposed.

Found by objectstack's full platform-checklist run (objectstack-ai/objectstack#23878), identity-auth slice — report comment 6216631966 on #23878, record objectstack-ai/objectstack#23943 — where the runner listed it as an unfiled observation. Filed by the objectstack domain:devx seat 1 PM (objectstack seat post #6023) · os-sales · session_014eFvbvGK4FjXvscggDNTD4, on the maintainer's pointer that reported problems were left unfiled. ⛔ Not a claim; lane and grade are triage's. The runner did not check the mechanism against objectui main.

Reader who acts: the objectui seat that owns packages/auth / the console shell header.

Where to look first (a question for the dev, not a verified cause)

  • packages/auth/src/AuthProvider.tsx:635-667, refreshOrganizations: it asks the server for the active organization only if (orgs.length > 0 && !activeOrganization), and with exactly one membership it re-activates orgs[0] itself (the ADR-0081 single-membership repair). Measure first whether the header's organization comes from a client-held value (activeOrganization state, ActiveOrganizationStorage) that a server-side "no active org" answer never clears, or from that repair — and whether the repair then actually re-stamps the server session.
  • packages/app-shell/src/console/ConsoleShell.tsx:383-405 reads activeOrganization for the header.

Done when

  • After a refused switch and a reload, the header names the organization the server reports as active, or shows the no-active-organization state the shell already has (ConsoleShell.tsx:405) — never a stale one.
  • A pin drives the refused-switch-then-reload sequence and asserts header and server agree.

Dedupe

MCP search_issues on objectstack-ai/objectui, closed included, at this filing: header shows previous organization after refused set-active organization switch NO_ACTIVE_ORGANIZATION stale org label → 3 hits: #5719, #5731, #5750 (all closed; neighbouring organization-switch state defects, none this one).

Dedupe words: stale organization header after refused switch; NO_ACTIVE_ORGANIZATION header mismatch; set-active refused header.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p2 · domain:ui · area:identity · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T15:03Z

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.

    • Why p2: after a refused switch, the header names a workspace the server no longer has active, so org-scoped screens act on a context the user cannot see is gone. The refusal itself is right, and nothing is exposed.
    • Lane: domain:ui. The header reads the server's active-organization answer after reload. ⛔ It does not re-assert the cached one.
  2. added
    area:identityLogin and identity — sign-up, sessions, organization membership, SSO
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 16, 2026
  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 (priority:p2, area:identity; triage first grade 6218366640)
    Session: session_01XVp19E1MYUTFM3dnkwz8gB
    Account: baozhoutao (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-12698-stale-active-org
    Worktree: objectui-issue-12698
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: packages/auth/src/AuthProvider.tsx (refreshOrganizations and how the active organization is derived after a reload); packages/auth/src/ActiveOrganizationStorage.ts only if the stale value is held there; packages/app-shell/src/console/ConsoleShell.tsx only where the header reads the active organization; tests beside each; one .changeset/12698-*.md (stop on breach; explain in the report). ⛔ Not LoginForm.tsx or SocialSignInButtons.tsx (open PR objectui#12761).
    Container & model: S, mode:subagent, model: opus (default tier; dispatch-gates --tier --repo objectstack-ai/objectui reads no path-derived mandate for these paths)
    Clause-②: no
    Responsibility: objectui @object-ui/auth: the client keeps or re-derives an active organization the server no longer reports (measured first: a client-held value never cleared, or the single-membership repair) | the server's own NO_ACTIVE_ORGANIZATION answer, which the shell already has a state for (ConsoleShell.tsx :405) | any member whose session loses its active organization (a refused switch, a removed membership); seen live on the stock showcase by objectstack-ai/objectstack#23878
    Thread-read: 6218366640
    Serial constraints cleared: scanned every open objectui PR's file list just now. The only packages/auth PR is objectui#12761 (seat 3's objectui#12682: LoginForm.tsx, SocialSignInButtons.tsx), which is disjoint from this surface. This seat's in-flight work touches the marketplace page (PR #12763) and the grid seam (PR #12756), both disjoint.
    Revised 2026-10-16T19:05Z (seat ruling on the dev's open question in os-dev report 6222585880): the surface adds packages/app-shell/src/layout/WorkspaceSwitcher.tsx and packages/app-shell/src/layout/CurrentOrganizationIndicator.tsx, with tests beside them. The measured cause is two client guesses: the stored id survives a server "none" (fixed in AuthProvider), and both header components render activeOrganization ?? orgList[0]. Ruling: option A. Drop the ?? orgList[0] fallback in both. With no server-active organization, the header shows the existing no-organization-context state, and org-scoped routes keep redirecting through RequireOrganization. No new copy, no new state, no export change. The same PR then closes the card. Serial check: no open objectui PR touches either component (scanned 2026-10-16T19:04Z).
    Claimed at: 2026-10-16T18:43Z

  4. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 12698,
      "status": "blocked",
      "branch": "claude/issue-12698-stale-active-org",
      "pr": "https://github.com/objectstack-ai/objectui/pull/12765",
      "session": "session_01XVp19E1MYUTFM3dnkwz8gB",
      "premise_still_valid": true,
      "summary": "Read the dispatch order (md5 e20574ae702e16fabca5481c622a1827); the newest Claim, comment 6222368818, names this branch. Root cause, measured with the real AuthProvider and the real header components against a mocked server that holds no active organization. better-auth 1.7.2 set-active nulls the session's active org BEFORE it refuses a non-member, so the server truly holds none. The client then gets two things wrong. (1) The header guesses. WorkspaceSwitcher.tsx:63 and CurrentOrganizationIndicator.tsx:61 render `activeOrganization ?? orgList[0]`. With two memberships, AuthProvider held null and the switcher still read 'Plant A'. With one membership and the repair refused, the indicator still read 'Plant B'. (2) The stored id survives. AuthProvider.refreshOrganizations heard 'none' and left ActiveOrganizationStorage = org_b, which kept stamping X-Tenant-ID and the MetadataProvider cache scope. A1 is false: activeOrganization is not hydrated from storage on load (it starts as useState(null)), and the server is asked once on reload. The ADR-0081 repair is already truthful: one set-active call, and the server's answer is adopted. Draft PR #12765 fixes (2) inside the surface: AuthProvider clears ActiveOrganizationStorage when the server-asking branch ends with no active organization, including a refused repair. A guard on activeOrgIdRef keeps an in-flight user switch from being wiped. Pins (a)-(d) are added, and the ablation turns (a) red. BLOCKED on (1): the fix is to drop the `?? orgList[0]` fallback in packages/app-shell/src/layout/WorkspaceSwitcher.tsx and CurrentOrganizationIndicator.tsx, with pins beside them. Both files are outside the dispatched file surface ('Anything else: stop and report'), so I did not edit them. The PR therefore says `Part of #12698`, not `Fixes`. That conflicts with the dispatch's `Fixes #12698`; I followed the role file's rule that a merge which leaves a half open must not close the card. Resume from claude/issue-12698-stale-active-org@d45dbf32f once the PM extends the surface.",
      "tests": "All at d45dbf32f unless noted; exit codes recorded in scratchpad issue-12698/gates.txt. New pin packages/auth/src/__tests__/staleActiveOrg-12698.test.tsx: 6 passed (a two-memberships, a end-to-end forge-then-boot, b repair re-stamps once, b repair refused, c control with no extra call, d control user switch). Ablation: the clear() line was replaced by a marker. The marker grep -c went 0 to 1, numstat 1 1, and three tests went red: (a)x2 and (b-refused) failed; (b), (c) and (d) stayed green. Restore used git checkout HEAD --, the HEAD blob 8f04201076f0 matched the restored file, and git diff HEAD was empty; a trap covered EXIT INT TERM. The test imports ../AuthProvider from src, so no dist was involved. Whole package: vitest run --maxWorkers=1 packages/auth/ exit 0, 32 files, 303 tests. pnpm --filter @object-ui/auth type-check exit 0, and tsconfig.test.json lists the new test (--listFiles count 1). The first type-check exited 2 on a wrong type import; that was fixed in de1e61191. The closure build --filter '@object-ui/auth^...' matched no projects, so it ran nothing. ESLint narrowed to the 2 changed files (population per package eslint . under the root eslint.config.js; count read from --format json): 0 errors and 4 warnings, all present on base 7ead04904 shifted by +12 lines. There is no type-aware linting (no parserOptions.project or projectService), so untouched files' verdicts are invariant. Battery, all exit 0: check:phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity, control-bytes, check-hand-rolled-comment-mask.mjs, check-changeset-fixed.mjs. check:spec-floors was not run (not required). The root-cause measurement was a scratch app-shell test (real AuthProvider plus WorkspaceSwitcher and CurrentOrganizationIndicator), kept out of the tree at scratchpad issue-12698/zzMeasure12698.test.tsx. CI: in_progress, not awaited.",
      "mcp_calls": "0 — no MCP GitHub tools used",
      "api_writes": "3 — git push of the branch (4 pushes: empty branch, then 3 commits); fleet-write relay pr_create (POST /repos/objectstack-ai/objectui/pulls, draft, plus POST /issues/12765/assignees baozhoutao); fleet-write relay comment (POST /repos/objectstack-ai/objectui/issues/12698/comments, this report). No label writes: objectui path labels belong to labeler.yml and the dispatch named none.",
      "gates": "type-check 0 · auth vitest dir 0 · pin 0 · eslint narrowed 0 (warnings = base) · pre-tsc battery 16/16 exit 0 · check-changeset-fixed 0 · closure build: no projects matched",
      "files_changed": [".changeset/12698-stale-active-org.md", "packages/auth/src/AuthProvider.tsx", "packages/auth/src/__tests__/staleActiveOrg-12698.test.tsx"],
      "deviations": "PR body says `Part of #12698` instead of the dispatch's `Fixes #12698`, because the header half is unshipped (role file Definition of done). I ran `git stash list` once, a read-only listing; no stash was created. A temporary copy of base AuthProvider was linted inside packages/auth/src for the warning comparison and deleted at once; it was never staged or committed.",
      "open_questions": [
        {
          "question": "Extend #12698's file surface to packages/app-shell/src/layout/WorkspaceSwitcher.tsx and CurrentOrganizationIndicator.tsx (plus tests beside them), and decide what the header shows with memberships but no server-active organization.",
          "options": [
            "A: drop the `?? orgList[0]` fallback in both components and render nothing when activeOrganization is null. WorkspaceSwitcher's existing 'No organization context' branch already does this. Org-scoped routes redirect to /organizations through RequireOrganization. No new copy, no export change, about 2 lines plus pins.",
            "B: drop the fallback, but have WorkspaceSwitcher (two or more memberships) render a neutral 'Choose organization' trigger with the list. This keeps the switch affordance on /organizations and /, but it adds an i18n key in en and zh plus a new state, about 30 lines.",
            "C: leave the header as is. The card's done-when stays unmet."
          ],
          "recommendation": "A. Real business need: the measured defect is the header naming an org the server does not hold, and A removes exactly that; nothing pulls for a new trigger state yet. Long-term soundness: the server is the authority, and A deletes a client guess instead of reshaping it. Preventing AI-authoring errors: nothing in metadata changes either way, and A removes a lenient consumer fallback (`??`). Startup focus: A adds no surface or copy, while B grows chrome without demand."
        }
      ],
      "out_of_scope_findings": [
        "carrier: whoever takes the header half of #12698 · refreshOrganizations skips the server when the membership list is empty, so a stored id with zero memberships is not cleared; not measured as reachable · noted in PR Acceptance notes, not filed"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:ui seat 2 · session_01XVp19E1MYUTFM3dnkwz8gB · 2026-10-16T19:45Z

    PR #12765 merged through the merge queue as 6e141d9, which is on main. Verified by content: all 7 of the PR's files on origin/main are blob-identical to the accepted head a405599. The squash commit's message names no model.

    What landed: after a refused organization switch and a reload, the console follows the server's answer.

    • AuthProvider clears the stored organization id when the server reports no active organization, so the old id no longer stamps X-Tenant-ID or keys the metadata cache.
    • The header's switcher and current-organization indicator name only the server-active organization. They no longer fall back to the first membership.
    • The ADR-0081 single-membership repair is unchanged: it was already truthful.

    Acceptance note, not filed: a stored id with zero memberships is not cleared, because refreshOrganizations skips the server call then. It was not measured as reachable.

    pm:dispatched leaves with this note. The card closed completed on merge.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:identityLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions