Skip to content

fix(auth): resolve seat via /v1/auth/status instead of JWT roles - #88

Merged
bergetbjork merged 4 commits into
mainfrom
fix/seat-status-check
Sep 7, 2026
Merged

bergetbjork merged 4 commits into
mainfrom
fix/seat-status-check

Conversation

@bergetbjork

Copy link
Copy Markdown
Contributor

Why the first fix wasn't enough (support case follow-up)

PR #84 taught the CLI to accept all seat roles — but a Pro subscriber still got 'no subscription'. Root cause found in prod data: Pro/Summit seats never get a Keycloak role at all (seat-product.config deliberately omits keycloakRole for the newer tiers — 'berget.seat-only'), and roles drift anyway (a Summit subscriber carried a stale berget_code_seat role — verified live via /v1/auth/status).

The API already authorizes inference against berget.seat (Odoo), not roles. The CLI was the last consumer reading roles.

Change

  • configureAuth routes on GET /v1/auth/status (Bearer token) → seatId/tier — the same canonical source the API authorizes against
  • Seat → tier-aware prompt ("You have a Berget Pro subscription…"); no seat → API-key path; unverifiable → warn + sync OAuth (existing pattern)
  • hasBergetCodeSeat + role list removed — the CLI no longer reads JWT roles anywhere
  • New SeatStatusPort (ports/adapters pattern), production impl never throws

No backend changes needed

/v1/auth/status already exposes seatId/tier for every tier. No Keycloak role creation, no backfill — aligned with moving away from seat roles entirely.

Tests (TDD)

New seat-status service tests (seat/no-seat/401/network/env-override); auth-sync cases re-routed through a FakeSeatStatusService (seat / no-seat / unverifiable); dead-code tests removed. 54/54 in touched suites; remaining suite failures are pre-existing on main (missing openid-client in node_modules, middleware test — verified via stash).

The CLI gated 'berget code init' on hasBergetCodeSeat(JWT) — a Keycloak
realm-role check. That broke Pro/Summit subscribers twice over: the
config deliberately omits keycloakRole for the newer tiers (berget.seat-
only), so those users never got ANY seat role, and roles drift anyway
(a Summit subscriber can carry a stale berget_code_seat role — verified
live). The API already authorizes inference against berget.seat (Odoo),
not roles.

configureAuth now asks GET /v1/auth/status (same canonical source the
API authorizes against) and routes on tier:
- seat → 'You have a <Tier> subscription' (tier-aware message)
- no seat → API-key path (unchanged)
- status unverifiable (network/5xx) → warn + sync OAuth anyway (same
  behavior as the old undecodable-JWT path)

hasBergetCodeSeat and its role list are removed — roles are no longer
read anywhere in the CLI. New SeatStatusPort keeps the ports/adapters
pattern; production impl never throws (null on any failure).

No backend changes needed: /v1/auth/status already exposes
seatId/tier resolved from berget.seat for every tier.
Comment thread src/auth/seat-status.ts Outdated
export function createSeatStatusService(): SeatStatusPort {
return {
async fetchSeatStatus(accessToken: string) {
const base = process.env.BERGET_API_URL || 'https://api.berget.ai';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 warning — Duplicates getAuthConfig() API-URL resolution and diverges in --local mode (localhost:3000 vs prod); reuse getAuthConfig().apiBaseUrl.

async fetchSeatStatus(accessToken: string) {
const base = process.env.BERGET_API_URL || 'https://api.berget.ai';
try {
const res = await fetch(`${base}/v1/auth/status`, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 warning — fetch has no timeout/AbortSignal — a hung request blocks the interactive init flow indefinitely; use AbortSignal.timeout(10_000).

Comment thread src/commands/code/auth-sync.ts Outdated
return handleUnverifiedAuth(prompter, files, homeDir, tool, cliAuth);
}

if (seatStatus.tier) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 warning — Gating on tier only: {seatId: 168, tier: null} sends a real seat-holder to "You do not have a Berget subscription"; check seatId too.

* Never throws: any failure (network, non-OK, bad payload) returns null so
* the caller can fall back to a warn-and-continue path.
*/
export function createSeatStatusService(): SeatStatusPort {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit — Implementation of a commands/code port lives in src/auth (inverted auth→commands type dependency); existing adapters live in commands/code/adapters or src/services.

@berget-ai

berget-ai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Review

Summary

This PR replaces offline JWT-role-based seat detection (hasBergetCodeSeat) with a live API lookup (GET /v1/auth/status) behind a new SeatStatusPort, adding a slow-fail "unverified" path and tier-aware prompt labels.

Risk

MEDIUM — The switch from offline to an online seat check is the right call for eliminating role drift, but the PR as written does not typecheck (npm run build fails), and the new network call introduces a hang risk and a config-drift edge in --local/stage environments.

Issues

  • critical src/commands/code/auth-sync.ts:50configureAuth accepts Pick<AuthDeps, 'apiKeyService' | 'files' | 'homeDir' | 'prompter'> but line 88 accesses deps.seatStatusService, which is not in that Pick → TS2339; tsc --noEmit/tsc fails. Tests pass only because vitest strips types without checking them and the test double is cast as AuthDeps (which hides the error by passing a superset object). Fix:

    deps: Pick<AuthDeps, 'apiKeyService' | 'files' | 'homeDir' | 'prompter' | 'seatStatusService'>,
  • warning src/auth/seat-status.ts:16 — Duplicates the API base-URL resolution that already exists in getAuthConfig() (src/auth/config.ts:6,20,34) and diverges from it: getAuthConfig({ local: true }) returns http://localhost:3000 (used by npm start --local and createAuthenticatedClient), so in local dev every other API call hits localhost while this one silently hits production api.berget.ai. Reuse the config: const base = getAuthConfig().apiBaseUrl;

  • warning src/auth/seat-status.ts:18 — The fetch has no timeout/abort signal, so a hung connection blocks the interactive init flow indefinitely; the codebase already handles this in pkce-flow.ts. Fix: signal: AbortSignal.timeout(10_000).

  • warning src/commands/code/auth-sync.ts:96 — The seat gate keys off seatStatus.tier only; a response of { seatId: 168, tier: null } (seat without a recognized plan, or partial payload — the port types both fields independently nullable and the TIER_LABELS[tier] ?? 'Berget' fallback shows tier values are not a closed set) routes an actual seat-holder to handleNoSeat and tells them "You do not have a Berget subscription". Gate on the seat itself, e.g. if (seatStatus.seatId ?? seatStatus.tier) passing seatStatus.tier ?? '' onward.

  • nit src/auth/seat-status.ts:13 — The production implementation of a commands/code/ports interface lives in src/auth and type-imports from src/commands/code, creating an inverted auth→feature dependency; existing adapters live in src/commands/code/adapters/ (clack-prompter, fs-file-store, …) or src/services/.

  • nit src/commands/code/auth-services.ts (ports docstring says "network/5xx/401") — 401 (revoked/invalid token) intentionally falls through to handleUnverifiedAuth, which syncs the already-invalid OAuth tokens into the tool config and reports "Authenticated." — acceptable per the documented fail-open design, but worth an explicit test for the 401 branch to pin that behavior down (the existing test only covers generic ok: false).

Suggestions

  • src/commands/code/auth-sync.ts — add 'seatStatusService' to the Pick as shown above (this also documents the dependency of configureAuth in its signature).
  • src/auth/seat-status.tsconst base = getAuthConfig().apiBaseUrl.replace(/\/$/, '') to reuse the single source of truth for API URL env handling.
  • src/auth/__tests__/seat-status.test.ts — consider vi.stubEnv for BERGET_API_URL to match the pattern already used in config.test.ts.

Architecture

Good port/adapter decomposition (net-offline auth module, injectable seat-status), though the placement of the real adapter in src/auth instead of the established adapter locations slightly inverts the layering.

CodeSense score: 7.5/10 — clean structure and well-faked tests, but it ships a signature/type mismatch that breaks the build and re-implements existing config resolution.

Inline findings


Berget AI (berget/zai-org/GLM-5.3-Flash) | PR #88

- configureAuth Pick now includes seatStatusService (was TS2339 build
  break, hidden by vitest's type-stripping)
- reuse getAuthConfig().apiBaseUrl instead of duplicating env resolution
  (diverged in --local mode: localhost:3000 vs prod)
- AbortSignal.timeout(10s) so a hung connection can't block init
- seat gate keys on seatId OR tier: {seatId, tier: null} no longer sends
  a real seat-holder to the 'no subscription' path (test pinned)
- move adapter to commands/code/adapters/ (matches clack-prompter etc.,
  removes inverted auth→commands dependency)
- vi.stubEnv in tests (matches config.test.ts pattern)
Comment thread src/commands/code/init.ts
homeDir: os.homedir(),
isTty: process.stdin.isTTY,
prompter: new ClackPrompter(),
seatStatusService: createSeatStatusService(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 warningWizardDeps has no seatStatusService property (TS2353) — npm run build fails; declaring the field on the interface (same fix as line 99) clears this.


afterEach(() => {
vi.unstubAllGlobals();
vi.unstubAllEnvs();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nitvi.unstubAllEnvs() restores stubs but no longer deletes a real BERGET_API_URL (the old afterEach did), so the default-URL assertions fail wherever that env var is set — delete it in beforeEach.

@berget-ai

berget-ai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Review

Follow-up review

New changes: 1 commit — d13eff9 "fix: address AI review — build break, config reuse, timeout, seat gate" (Pick fix, getAuthConfig reuse, 10s timeout, seatId gate, adapter relocated to src/commands/code/adapters/, vi.stubEnv, new {seatId, tier: null} regression test).

Previous findings:

  • ♻️ critical Fix/chat fixes #1 (TS2339 in configureAuth Pick) — partially fixed: original error gone, but the
    build is still red; the missing dep now surfaces as two new tsc errors in init.ts (new findings below).
  • ✅ warning (config reuse) fixed — now getAuthConfig().apiBaseUrl, env precedence consistent with src/auth/config.ts.
  • ✅ warning (no timeout) fixed — AbortSignal.timeout(10_000); engines node >=22, abort lands in the existing catch → null.
  • ✅ warning (tier-only gate) fixed — seatId != null || tier != null, label falls back to 'Berget' (TIER_LABELS is Record<string,string>), pinned by the new {seatId: 168, tier: null} test.
  • ✅ nit (adapter placement / inverted auth→feature dep) fixed — src/commands/code/adapters/seat-status.ts, matches clack-prompter/fs-file-store, no stale imports.
  • ⏳ nit (explicit 401 fall-through test) still open — 401 only reaches the covered generic null path.

New findings:

  • 🔴 src/commands/code/init.ts:99configureAuth is called without seatStatusService: TS2345, and at runtime deps.seatStatusService is undefinedTypeError: Cannot read properties of undefined (reading 'fetchSeatStatus') at auth-sync.ts:88 on the normal valid-JWT path. This is not theoretical: npx vitest run fails — 3 of the repo's own runInit integration tests in init.test.ts crash (265 tests: 3 failed). Only the two touched test files (43/43) were green because the author evidently didn't run the full suite. Fix:
    export interface WizardDeps { ...; seatStatusService: SeatStatusPort; }
    const { ..., prompter, seatStatusService } = deps;
    await configureAuth({ apiKeyService, files, homeDir, prompter, seatStatusService }, tool, cliAuth);
  • 🟠 src/commands/code/init.ts:133seatStatusService passed into executeInitCommand but WizardDeps (init.ts:31–40) doesn't declare it → TS2353, breaking npm run build; same interface fix clears it.
  • 🟡 src/auth/__tests__/seat-status.test.ts:9vi.unstubAllEnvs() restores stubs but (unlike the old afterEach) no longer deletes a real BERGET_API_URL, so the default-URL assertions break in any environment where that var is set.

All four substantive code fixes were verified directly and are correct in isolation — the follow-up commit simply swapped one type error for a type and runtime error at the wiring site, so the PR remains unmergeable. CodeSense unchanged at 7.5.

Suggestions

  • src/commands/code/init.ts — add seatStatusService: SeatStatusPort to WizardDeps (import it from ./ports/auth-services.js alongside the existing ports), destructure in runInit, pass to configureAuth.

Inline findings


Berget AI (berget/zai-org/GLM-5.3-Flash) | PR #88

tsc caught what local type-stripping hid: WizardDeps lacked the field
and the configureAuth call site in runInit didn't pass it. init.test.ts
makeDeps now defaults to a no-seat fake; seat-path tests override.
berget-ai[bot]
berget-ai Bot previously approved these changes Sep 7, 2026

@berget-ai berget-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Follow-up review

New changes: 1 commit (21061ae) — adds seatStatusService to WizardDeps, destructures it in runInit, and passes it through to configureAuth; test makeDeps now defaults a null-seat FakeSeatStatusService.

Previous findings:

  • ✅ critical (init.ts configureAuth missing seatStatusService → TS2345 + runtime TypeError + 3 crashed tests) — fixed: declared at init.ts:40, destructured :67, passed :110, runInitCommand wires it :144; verified locally: tsc --noEmit exit 0, vitest run 265/265 passed (was 3 failed).
  • ✅ warning (seatStatusService passed to executeInitCommand but absent from WizardDeps → TS2353) — fixed by the same interface change.
  • ✅ critical #1 from first review (TS2339 in configureAuth Pick) — now fully fixed (typecheck clean).
  • ✅ warnings (config reuse, 10s timeout, seatId||tier gate) — fixed earlier, still correct in this wiring.
  • ⏳ nit (seat-status.test.ts:9vi.unstubAllEnvs() doesn't delete a real BERGET_API_URL, so default-URL asserts break in envs where it's set) — still present, file untouched by this commit.
  • ⏳ nit (explicit 401 fall-through test) — still open, auth-sync.test.ts untouched.

New findings: None. The test-helper spread exclusion list omits seatStatusService, but the double-set is with the identical value (overrides.seatStatusService ?? default on both sides) so it's inert; the two explicit seated fakes at init.test.ts:581/737 are redundant with makeDeps overrides but intentional for the seat-path tests.

Done right: the breaking wiring is now exercised by the repo's own runInit suite via the makeDeps default instead of crashing.

Inline findings


Berget AI (berget/zai-org/GLM-5.3-Flash) | PR #88

Comment thread src/commands/code/init.ts
homeDir: string;
isTty?: boolean;
prompter: Prompter;
seatStatusService: SeatStatusPort;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good — Fixes both prior findings: seatStatusService is now a required WizardDeps field and is threaded to configureAuth (line 110); verified tsc clean and 265/265 tests green.

homeDir: '/home/user',
isTty: overrides.isTty ?? true,
prompter: overrides.prompter ?? new FakePrompter([]),
seatStatusService:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good — Defaulting a null-seat FakeSeatStatusService in makeDeps repairs the three previously crashing runInit tests without weakening their assertions.

@berget-ai berget-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Follow-up review

New changes: 1 commit (2820d89) — pure eslint --fix style pass: prettier line-wrapping and perfectionist reorderings (imports, object keys, union member order) across 7 files; zero semantic delta.

Previous findings:

  • ✅ blocker configureAuth missing seatStatusService (TS2345 + TypeError + 3 crashed tests) — fixed.
  • ✅ warning seatStatusService absent from WizardDeps (TS2353) — fixed.
  • ✅ blocker TS2339 in configureAuth Pick — fixed.
  • ✅ warnings (config reuse, 10s AbortSignal.timeout, seatId||tier gate) — fixed, unchanged and still correct.
  • ⏳ nit seat-status.test.ts vi.unstubAllEnvs() doesn't remove a real inherited BERGET_API_URL, so default-URL asserts break in envs where it's set — still present; this commit touched the file but only reordered mock-object keys, leaving afterEach unhydrated.
  • ⏳ nit explicit 401 fall-through test for the auth-sync seat path — still open; auth-sync.test.ts touched only by import reorder.

New findings: None — union reordering (null | SeatStatus), object-key ordering, and import sorting are inert in TS/JS. Verified: tsc --noEmit exit 0 and vitest run 23 files / 265/265 passed.

Done right: the style pass wasted no chance to sneak behavioral edits — every hunk is provably mechanical, confirmed by the green suite.

Inline findings


Berget AI (berget/zai-org/GLM-5.3-Flash) | PR #88

@bergetbjork
bergetbjork merged commit 2752bb4 into main Sep 7, 2026
2 checks passed
@bergetbjork
bergetbjork deleted the fix/seat-status-check branch September 7, 2026 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant