trypost/tests/Feature/McpOAuthWorkspaceBindingTest.php

362 lines
14 KiB
PHP
Raw Normal View History

Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
<?php
declare(strict_types=1);
use App\Enums\UserWorkspace\Role;
use App\Models\AccessToken;
use App\Models\User;
use App\Models\Workspace;
use App\Passport\AccessTokenRepository;
use App\Passport\AuthCode;
use App\Passport\AuthCodeRepository;
use App\Passport\OAuthPayloadDecryptor;
use Illuminate\Support\Str;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
beforeEach(function () {
$this->user = User::factory()->create();
$this->workspace = Workspace::factory()->create([
'account_id' => $this->user->account_id,
'user_id' => $this->user->id,
]);
$this->workspace->members()->attach($this->user->id, ['role' => Role::Admin->value]);
$this->user->update(['current_workspace_id' => $this->workspace->id]);
$this->user->refresh();
$this->clientId = mcpOauthClient();
$this->decryptor = app(OAuthPayloadDecryptor::class);
});
test('authorization code grant binds workspace from the encrypted auth code payload', function () {
$authCodeId = Str::random(80);
$tokenId = Str::random(80);
AuthCode::query()->forceCreate([
'id' => $authCodeId,
'user_id' => $this->user->id,
'client_id' => $this->clientId,
'workspace_id' => $this->workspace->id,
'scopes' => '[]',
'revoked' => true,
'expires_at' => now()->addMinutes(10),
]);
// User switched workspace between consent and token exchange — auth code wins.
$otherWorkspace = Workspace::factory()->create([
'account_id' => $this->user->account_id,
'user_id' => $this->user->id,
]);
$otherWorkspace->members()->attach($this->user->id, ['role' => Role::Admin->value]);
$this->user->update(['current_workspace_id' => $otherWorkspace->id]);
request()->merge([
'grant_type' => 'authorization_code',
'code' => $this->decryptor->encrypt([
'client_id' => $this->clientId,
'auth_code_id' => $authCodeId,
'user_id' => (string) $this->user->id,
'scopes' => [],
'expire_time' => now()->addMinutes(10)->timestamp,
]),
]);
persistAccessTokenEntity($tokenId, (string) $this->user->id, $this->clientId);
$token = AccessToken::query()->findOrFail($tokenId);
expect($token->workspace_id)->toBe($this->workspace->id);
});
test('refresh grant inherits workspace from the refreshed access token id', function () {
$previous = mcpAccessToken($this->user, $this->clientId, $this->workspace);
$tokenId = Str::random(80);
$otherWorkspace = Workspace::factory()->create([
'account_id' => $this->user->account_id,
'user_id' => $this->user->id,
]);
$otherWorkspace->members()->attach($this->user->id, ['role' => Role::Admin->value]);
// Newer grant on another workspace for the same client must not win.
mcpAccessToken($this->user, $this->clientId, $otherWorkspace);
$this->user->update(['current_workspace_id' => $otherWorkspace->id]);
request()->merge([
'grant_type' => 'refresh_token',
'refresh_token' => $this->decryptor->encrypt([
'client_id' => $this->clientId,
'refresh_token_id' => Str::random(80),
'access_token_id' => $previous->id,
'scopes' => [],
'user_id' => (string) $this->user->id,
'expire_time' => now()->addMonth()->timestamp,
]),
]);
persistAccessTokenEntity($tokenId, (string) $this->user->id, $this->clientId);
$refreshed = AccessToken::query()->findOrFail($tokenId);
expect($refreshed->workspace_id)->toBe($previous->workspace_id)
->and($refreshed->workspace_id)->not->toBe($otherWorkspace->id);
});
test('refresh grant fails when the user no longer belongs to the bound workspace', function () {
$previous = mcpAccessToken($this->user, $this->clientId, $this->workspace);
$tokenId = Str::random(80);
$this->workspace->members()->detach($this->user->id);
request()->merge([
'grant_type' => 'refresh_token',
'refresh_token' => $this->decryptor->encrypt([
'client_id' => $this->clientId,
'refresh_token_id' => Str::random(80),
'access_token_id' => $previous->id,
'scopes' => [],
'user_id' => (string) $this->user->id,
'expire_time' => now()->addMonth()->timestamp,
]),
]);
expect(fn () => persistAccessTokenEntity($tokenId, (string) $this->user->id, $this->clientId))
->toThrow(OAuthServerException::class);
expect(AccessToken::query()->find($tokenId))->toBeNull();
});
test('unexpected grant types fail closed instead of using the current workspace', function () {
$tokenId = Str::random(80);
request()->merge(['grant_type' => 'client_credentials']);
expect(fn () => persistAccessTokenEntity($tokenId, (string) $this->user->id, $this->clientId))
->toThrow(OAuthServerException::class);
expect(AccessToken::query()->find($tokenId))->toBeNull();
});
test('authorization code grant fails when the user left the auth code workspace before exchange', function () {
$authCodeId = Str::random(80);
$tokenId = Str::random(80);
AuthCode::query()->forceCreate([
'id' => $authCodeId,
'user_id' => $this->user->id,
'client_id' => $this->clientId,
'workspace_id' => $this->workspace->id,
'scopes' => '[]',
'revoked' => true,
'expires_at' => now()->addMinutes(10),
]);
$this->workspace->members()->detach($this->user->id);
request()->merge([
'grant_type' => 'authorization_code',
'code' => $this->decryptor->encrypt([
'client_id' => $this->clientId,
'auth_code_id' => $authCodeId,
'user_id' => (string) $this->user->id,
'scopes' => [],
'expire_time' => now()->addMinutes(10)->timestamp,
]),
]);
expect(fn () => persistAccessTokenEntity($tokenId, (string) $this->user->id, $this->clientId))
->toThrow(OAuthServerException::class);
expect(AccessToken::query()->find($tokenId))->toBeNull();
});
test('personal access tokens are left alone for controllers to bind', function () {
$result = $this->user->createToken('PAT');
$token = AccessToken::query()->find($result->token->id);
expect($token->workspace_id)->toBeNull();
});
test('auth code repository captures workspace_id from the consent form', function () {
$this->actingAs($this->user);
request()->merge(['workspace_id' => $this->workspace->id]);
$client = Mockery::mock(ClientEntityInterface::class);
$client->shouldReceive('getIdentifier')->andReturn($this->clientId);
$entity = Mockery::mock(AuthCodeEntityInterface::class);
$entity->shouldReceive('getIdentifier')->andReturn(Str::random(80));
$entity->shouldReceive('getUserIdentifier')->andReturn((string) $this->user->id);
$entity->shouldReceive('getClient')->andReturn($client);
$entity->shouldReceive('getScopes')->andReturn([]);
$entity->shouldReceive('getExpiryDateTime')->andReturn(now()->addMinutes(10)->toDateTimeImmutable());
app(AuthCodeRepository::class)->persistNewAuthCode($entity);
$stored = AuthCode::query()->where('client_id', $this->clientId)->first();
expect($stored)->not->toBeNull();
expect($stored->workspace_id)->toBe($this->workspace->id);
});
test('auth code repository prefers workspace_id from the consent form', function () {
$otherWorkspace = Workspace::factory()->create([
'account_id' => $this->user->account_id,
'user_id' => $this->user->id,
'name' => 'Workspace B',
]);
$otherWorkspace->members()->attach($this->user->id, ['role' => Role::Admin->value]);
// Current workspace stays A; consent form picks B.
$this->actingAs($this->user);
request()->merge(['workspace_id' => $otherWorkspace->id]);
$client = Mockery::mock(ClientEntityInterface::class);
$client->shouldReceive('getIdentifier')->andReturn($this->clientId);
$entity = Mockery::mock(AuthCodeEntityInterface::class);
$entity->shouldReceive('getIdentifier')->andReturn(Str::random(80));
$entity->shouldReceive('getUserIdentifier')->andReturn((string) $this->user->id);
$entity->shouldReceive('getClient')->andReturn($client);
$entity->shouldReceive('getScopes')->andReturn([]);
$entity->shouldReceive('getExpiryDateTime')->andReturn(now()->addMinutes(10)->toDateTimeImmutable());
app(AuthCodeRepository::class)->persistNewAuthCode($entity);
$stored = AuthCode::query()->where('client_id', $this->clientId)->first();
expect($stored->workspace_id)->toBe($otherWorkspace->id);
expect($this->user->fresh()->current_workspace_id)->toBe($this->workspace->id);
});
test('auth code repository rejects a consent workspace the user does not belong to', function () {
$foreign = Workspace::factory()->create();
$this->actingAs($this->user);
request()->merge(['workspace_id' => $foreign->id]);
$client = Mockery::mock(ClientEntityInterface::class);
$client->shouldReceive('getIdentifier')->andReturn($this->clientId);
$entity = Mockery::mock(AuthCodeEntityInterface::class);
$entity->shouldReceive('getIdentifier')->andReturn(Str::random(80));
$entity->shouldReceive('getUserIdentifier')->andReturn((string) $this->user->id);
$entity->shouldReceive('getClient')->andReturn($client);
$entity->shouldReceive('getScopes')->andReturn([]);
$entity->shouldReceive('getExpiryDateTime')->andReturn(now()->addMinutes(10)->toDateTimeImmutable());
Activation checklist + MCP OAuth authorize UX (#239) (#250) * Wire onboarding activation into Account, observers, and shared Inertia data Add onboarding casts/hasFinishedOnboarding, AccessToken ObservedBy, Platform::connectableOptions, Post/SocialAccount onboarding broadcast hooks, and lazy onboardingResidual share + SharedData types. * Register onboarding routes and post-checkout activation redirects. Wire billing processing and the sidebar checklist so owners land on activation after subscribe, with locale sidebar/uk onboarding strings. * Align MCP grant usability with onboarding activation checks Unbound MCP tokens fall back to the user's current workspace and require createPost so viewer/unscoped grants neither unlock the checklist nor broadcast onboarding status. * Require bound MCP workspace for onboarding activation. Drop current-workspace fallback from usable MCP grants so checklist detection and broadcasts match Passport token scoping; viewers still cannot unlock the MCP step. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden onboarding review findings and tighten locale strings. Fix Welcome/Persona/TrackPost suites broken by the activation route reuse and PostObserver analytics side effects, restore Echo poll fallbacks, reject unbound MCP grants in tests, and drop unused onboarding.mcp keys. Co-authored-by: Cursor <cursoragent@cursor.com> * Remove unused sidebar and MCP authorization locale keys. Drop dead sidebar menu/theme strings (including the overwritten workspace label and api_keys nav entry) and unused MCP authorize app_title/approving copy across all locales. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix SetLocale crashing on Passport Symfony OAuth responses. OAuth errors return a raw Symfony Response without withCookie(); attach the default locale cookie via headers so authorize no longer 500s. Co-authored-by: Cursor <cursoragent@cursor.com> * Prompt OAuth guests to log in before rejecting unknown clients. MCP Inspector often reuses a stale client_id; validateAuthorizationRequest was returning invalid_client JSON before the login redirect. Guests now hit /login first, then client validation runs after authentication. Co-authored-by: Cursor <cursoragent@cursor.com> * Render Inertia OAuth authorize errors for browser logins. After login, Inertia follows the intended authorize URL; raw invalid_client JSON broke that visit. HTML/Inertia requests now get mcp/AuthorizeError while API JSON clients still receive the OAuth error payload. Co-authored-by: Cursor <cursoragent@cursor.com> * Detect Inertia OAuth error pages via Request::inertia(). Use the framework helper so post-login authorize failures keep returning an Inertia page instead of raw OAuth JSON. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify OAuth authorize error page detection to expectsJson. Drop the X-Inertia header sniff; browser and Inertia visits already do not expectsJson, while API clients still receive the OAuth JSON payload. Co-authored-by: Cursor <cursoragent@cursor.com> * Share MCP authorize layout and drop the error close button. Keep authorize and authorize-error on the same centered card shell instead of the auth split layout. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding activation for reviewability and safety. Use an exists-based MCP check, keep GETs read-only, move sync into syncAndNotify, clear MCP skips on connect, restrict complete to owners, and share Echo/poll via one composable. Co-authored-by: Cursor <cursoragent@cursor.com> * Move MCP OAuth authorize UX out of the onboarding PR. Keep the activation checklist focused; OAuth guest/error-page work now lives on fix/mcp-oauth-authorize-ux. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix corrupted French MCP locale after OAuth key cleanup. Co-authored-by: Cursor <cursoragent@cursor.com> * Restore MCP OAuth authorize UX onto the onboarding branch. Keep authorize error page, guest login-before-client validation, and SetLocale Symfony cookie fix in #250. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix OAuth prompt=none redirects and harden onboarding tests. Keep login_required/consent_required as redirects instead of Inertia, add regression coverage for owner-only activation, require invite email confirmation, and align MCP connected apps with the sessions list UI. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding guards and dedupe viewed analytics. Introduce isOnboardingOpen / belongsToAccount helpers, collapse duplicated sync/dispatch paths, and capture onboarding.viewed once per account. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding event, observers, and status helpers. Tighten Account onboarding predicates, drop nullable broadcast/dispatch APIs, and collapse repeated observer/controller guards. Co-authored-by: Cursor <cursoragent@cursor.com> * Treat in-app users as always having an account. Add resolveAccount(), tighten belongsToAccount to string ids, and fold guest residual handling into ResolveOnboardingStatus. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename onboarding residual share test to progress. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding status and rename residual to progress. Use accountOrFail, extract MCP onboarding scope, auto-leave the ready screen, and send non-onboarding checkout back to accounts. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract HasAccount and prefer data_get in onboarding flows. Move account helpers off User, drop nullable sidebarProgress, and read OAuth/onboarding payloads with data_get. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding checks and extract HasOnboarding. Use Eloquent + policies for MCP/backfill paths, and move account onboarding helpers into a dedicated trait. Co-authored-by: Cursor <cursoragent@cursor.com> * Add trait tests and tidy onboarding imports. Cover HasAccount and HasOnboarding under Models/Traits, prefer filled() for checkout session ids, and import Throwable instead of FQCN. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify checkout session_id and OAuth error props. Read session_id via request->string(), and take OAuth error details from the League exception instead of decoding the response body. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify PostObserver onboarding notify path. Share one otherPosts check for first-create and last-delete instead of separate callbacks. Co-authored-by: Cursor <cursoragent@cursor.com> * Use post author as onboarding sync actor. Drop Auth::user() preference in PostObserver; checklist sync attributes to $post->user. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify SocialAccountObserver and OAuth authorize flow. Share create/delete onboarding notify, drop Auth actor fallback to owner, and inline Passport Inertia error handling. Co-authored-by: Cursor <cursoragent@cursor.com> * Use lazy Inertia props for onboarding partial reloads. Drop partial-header branching; wrap page props in closures and always redirect completed/dismissed accounts to the calendar. Co-authored-by: Cursor <cursoragent@cursor.com> * Defer sidebar onboarding progress and stamp completion as owner-only. Skip the MCP checklist work on full Inertia visits via deferred shared props, early-exit token scans, and keep account completion stamps owner-gated. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify deferred onboarding progress share via canShowProgress. Co-authored-by: Cursor <cursoragent@cursor.com> * Add User firstName for shared auth and simplify onboarding page. Co-authored-by: Cursor <cursoragent@cursor.com> * Move User firstName coverage into UserTest. Co-authored-by: Cursor <cursoragent@cursor.com> * Use first_name directly without empty-name fallbacks. Co-authored-by: Cursor <cursoragent@cursor.com> * Resolve onboarding sample prompt on the frontend via i18n. Co-authored-by: Cursor <cursoragent@cursor.com> * Stamp onboarding completion via the account owner after teammate unlocks. Co-authored-by: Cursor <cursoragent@cursor.com> * Count only the account owner MCP grant toward onboarding activation. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix OAuth consent auth-token mismatch for mid-activation owners. Skip deferred onboardingProgress on Passport authorize so Inertia does not rotate the session authToken, cover happy and stale-token paths in tests, and polish MCP setup copy plus sidebar/onboarding layout. Co-authored-by: Cursor <cursoragent@cursor.com> * Keep users on onboarding after activation completes. Stamp completion and re-render the finished checklist instead of redirecting to the calendar so owners can review the done state. Co-authored-by: Cursor <cursoragent@cursor.com> * Clarify Passport consent-view opt-out and guard app-route deferral. Rename the authorize-only route check and assert onboardingProgress still defers on calendar, onboarding, and MCP settings. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden onboarding completion and MCP consent workspace binding. Reject OAuth approve without a workspace, retry auto-complete until stamped, send dismissed complete straight to calendar, and cover the device consent defer opt-out. Co-authored-by: Cursor <cursoragent@cursor.com> * Enable activation checklist for self-hosted installs. Remove the self-hosted onboarding redirects, keep the SaaS-only dismiss backfill, and cover subscription-less owners plus skip/complete destinations. Co-authored-by: Cursor <cursoragent@cursor.com> * Add GitHub, Hacker News, and directories referral sources. Expand the welcome referral step with open-source and directory discovery channels. Co-authored-by: Cursor <cursoragent@cursor.com> * Refine welcome referral sources and labels. Split Instagram/Threads, add Founder, and shorten Google, GitHub, AI, and blog option labels. Co-authored-by: Cursor <cursoragent@cursor.com> * Sort accounts platforms alphabetically and drop connect hover plus. Reuse connectableOptions for the accounts index and remove the unused plus badge on disconnected cards. Co-authored-by: Cursor <cursoragent@cursor.com> * Centralize PostHog once-capture so disabled installs don't burn dedupe keys. Move isEnabled + Cache::add into PostHogService::captureOnce and route onboarding viewed/step events through it. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding backfill to complete every existing open account. Drop self-hosted and subscription filters; down clears completed_at again. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop PostHog captureOnce and use plain capture for onboarding. Remove cache-based event dedupe; callers rely on PostHogService::capture gating. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 23:34:43 +00:00
expect(fn () => app(AuthCodeRepository::class)->persistNewAuthCode($entity))
->toThrow(OAuthServerException::class);
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
Activation checklist + MCP OAuth authorize UX (#239) (#250) * Wire onboarding activation into Account, observers, and shared Inertia data Add onboarding casts/hasFinishedOnboarding, AccessToken ObservedBy, Platform::connectableOptions, Post/SocialAccount onboarding broadcast hooks, and lazy onboardingResidual share + SharedData types. * Register onboarding routes and post-checkout activation redirects. Wire billing processing and the sidebar checklist so owners land on activation after subscribe, with locale sidebar/uk onboarding strings. * Align MCP grant usability with onboarding activation checks Unbound MCP tokens fall back to the user's current workspace and require createPost so viewer/unscoped grants neither unlock the checklist nor broadcast onboarding status. * Require bound MCP workspace for onboarding activation. Drop current-workspace fallback from usable MCP grants so checklist detection and broadcasts match Passport token scoping; viewers still cannot unlock the MCP step. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden onboarding review findings and tighten locale strings. Fix Welcome/Persona/TrackPost suites broken by the activation route reuse and PostObserver analytics side effects, restore Echo poll fallbacks, reject unbound MCP grants in tests, and drop unused onboarding.mcp keys. Co-authored-by: Cursor <cursoragent@cursor.com> * Remove unused sidebar and MCP authorization locale keys. Drop dead sidebar menu/theme strings (including the overwritten workspace label and api_keys nav entry) and unused MCP authorize app_title/approving copy across all locales. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix SetLocale crashing on Passport Symfony OAuth responses. OAuth errors return a raw Symfony Response without withCookie(); attach the default locale cookie via headers so authorize no longer 500s. Co-authored-by: Cursor <cursoragent@cursor.com> * Prompt OAuth guests to log in before rejecting unknown clients. MCP Inspector often reuses a stale client_id; validateAuthorizationRequest was returning invalid_client JSON before the login redirect. Guests now hit /login first, then client validation runs after authentication. Co-authored-by: Cursor <cursoragent@cursor.com> * Render Inertia OAuth authorize errors for browser logins. After login, Inertia follows the intended authorize URL; raw invalid_client JSON broke that visit. HTML/Inertia requests now get mcp/AuthorizeError while API JSON clients still receive the OAuth error payload. Co-authored-by: Cursor <cursoragent@cursor.com> * Detect Inertia OAuth error pages via Request::inertia(). Use the framework helper so post-login authorize failures keep returning an Inertia page instead of raw OAuth JSON. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify OAuth authorize error page detection to expectsJson. Drop the X-Inertia header sniff; browser and Inertia visits already do not expectsJson, while API clients still receive the OAuth JSON payload. Co-authored-by: Cursor <cursoragent@cursor.com> * Share MCP authorize layout and drop the error close button. Keep authorize and authorize-error on the same centered card shell instead of the auth split layout. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding activation for reviewability and safety. Use an exists-based MCP check, keep GETs read-only, move sync into syncAndNotify, clear MCP skips on connect, restrict complete to owners, and share Echo/poll via one composable. Co-authored-by: Cursor <cursoragent@cursor.com> * Move MCP OAuth authorize UX out of the onboarding PR. Keep the activation checklist focused; OAuth guest/error-page work now lives on fix/mcp-oauth-authorize-ux. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix corrupted French MCP locale after OAuth key cleanup. Co-authored-by: Cursor <cursoragent@cursor.com> * Restore MCP OAuth authorize UX onto the onboarding branch. Keep authorize error page, guest login-before-client validation, and SetLocale Symfony cookie fix in #250. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix OAuth prompt=none redirects and harden onboarding tests. Keep login_required/consent_required as redirects instead of Inertia, add regression coverage for owner-only activation, require invite email confirmation, and align MCP connected apps with the sessions list UI. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding guards and dedupe viewed analytics. Introduce isOnboardingOpen / belongsToAccount helpers, collapse duplicated sync/dispatch paths, and capture onboarding.viewed once per account. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding event, observers, and status helpers. Tighten Account onboarding predicates, drop nullable broadcast/dispatch APIs, and collapse repeated observer/controller guards. Co-authored-by: Cursor <cursoragent@cursor.com> * Treat in-app users as always having an account. Add resolveAccount(), tighten belongsToAccount to string ids, and fold guest residual handling into ResolveOnboardingStatus. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename onboarding residual share test to progress. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding status and rename residual to progress. Use accountOrFail, extract MCP onboarding scope, auto-leave the ready screen, and send non-onboarding checkout back to accounts. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract HasAccount and prefer data_get in onboarding flows. Move account helpers off User, drop nullable sidebarProgress, and read OAuth/onboarding payloads with data_get. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding checks and extract HasOnboarding. Use Eloquent + policies for MCP/backfill paths, and move account onboarding helpers into a dedicated trait. Co-authored-by: Cursor <cursoragent@cursor.com> * Add trait tests and tidy onboarding imports. Cover HasAccount and HasOnboarding under Models/Traits, prefer filled() for checkout session ids, and import Throwable instead of FQCN. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify checkout session_id and OAuth error props. Read session_id via request->string(), and take OAuth error details from the League exception instead of decoding the response body. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify PostObserver onboarding notify path. Share one otherPosts check for first-create and last-delete instead of separate callbacks. Co-authored-by: Cursor <cursoragent@cursor.com> * Use post author as onboarding sync actor. Drop Auth::user() preference in PostObserver; checklist sync attributes to $post->user. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify SocialAccountObserver and OAuth authorize flow. Share create/delete onboarding notify, drop Auth actor fallback to owner, and inline Passport Inertia error handling. Co-authored-by: Cursor <cursoragent@cursor.com> * Use lazy Inertia props for onboarding partial reloads. Drop partial-header branching; wrap page props in closures and always redirect completed/dismissed accounts to the calendar. Co-authored-by: Cursor <cursoragent@cursor.com> * Defer sidebar onboarding progress and stamp completion as owner-only. Skip the MCP checklist work on full Inertia visits via deferred shared props, early-exit token scans, and keep account completion stamps owner-gated. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify deferred onboarding progress share via canShowProgress. Co-authored-by: Cursor <cursoragent@cursor.com> * Add User firstName for shared auth and simplify onboarding page. Co-authored-by: Cursor <cursoragent@cursor.com> * Move User firstName coverage into UserTest. Co-authored-by: Cursor <cursoragent@cursor.com> * Use first_name directly without empty-name fallbacks. Co-authored-by: Cursor <cursoragent@cursor.com> * Resolve onboarding sample prompt on the frontend via i18n. Co-authored-by: Cursor <cursoragent@cursor.com> * Stamp onboarding completion via the account owner after teammate unlocks. Co-authored-by: Cursor <cursoragent@cursor.com> * Count only the account owner MCP grant toward onboarding activation. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix OAuth consent auth-token mismatch for mid-activation owners. Skip deferred onboardingProgress on Passport authorize so Inertia does not rotate the session authToken, cover happy and stale-token paths in tests, and polish MCP setup copy plus sidebar/onboarding layout. Co-authored-by: Cursor <cursoragent@cursor.com> * Keep users on onboarding after activation completes. Stamp completion and re-render the finished checklist instead of redirecting to the calendar so owners can review the done state. Co-authored-by: Cursor <cursoragent@cursor.com> * Clarify Passport consent-view opt-out and guard app-route deferral. Rename the authorize-only route check and assert onboardingProgress still defers on calendar, onboarding, and MCP settings. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden onboarding completion and MCP consent workspace binding. Reject OAuth approve without a workspace, retry auto-complete until stamped, send dismissed complete straight to calendar, and cover the device consent defer opt-out. Co-authored-by: Cursor <cursoragent@cursor.com> * Enable activation checklist for self-hosted installs. Remove the self-hosted onboarding redirects, keep the SaaS-only dismiss backfill, and cover subscription-less owners plus skip/complete destinations. Co-authored-by: Cursor <cursoragent@cursor.com> * Add GitHub, Hacker News, and directories referral sources. Expand the welcome referral step with open-source and directory discovery channels. Co-authored-by: Cursor <cursoragent@cursor.com> * Refine welcome referral sources and labels. Split Instagram/Threads, add Founder, and shorten Google, GitHub, AI, and blog option labels. Co-authored-by: Cursor <cursoragent@cursor.com> * Sort accounts platforms alphabetically and drop connect hover plus. Reuse connectableOptions for the accounts index and remove the unused plus badge on disconnected cards. Co-authored-by: Cursor <cursoragent@cursor.com> * Centralize PostHog once-capture so disabled installs don't burn dedupe keys. Move isEnabled + Cache::add into PostHogService::captureOnce and route onboarding viewed/step events through it. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding backfill to complete every existing open account. Drop self-hosted and subscription filters; down clears completed_at again. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop PostHog captureOnce and use plain capture for onboarding. Remove cache-based event dedupe; callers rely on PostHogService::capture gating. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 23:34:43 +00:00
expect(AuthCode::query()->where('client_id', $this->clientId)->exists())->toBeFalse();
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
});
test('auth code repository requires workspace_id from the consent form', function () {
$otherWorkspace = Workspace::factory()->create([
'account_id' => $this->user->account_id,
'user_id' => $this->user->id,
'name' => 'Workspace B',
]);
$otherWorkspace->members()->attach($this->user->id, ['role' => Role::Admin->value]);
$this->user->update(['current_workspace_id' => $otherWorkspace->id]);
$this->actingAs($this->user->fresh());
// No workspace_id in the request — current workspace must not be used.
$client = Mockery::mock(ClientEntityInterface::class);
$client->shouldReceive('getIdentifier')->andReturn($this->clientId);
$entity = Mockery::mock(AuthCodeEntityInterface::class);
$entity->shouldReceive('getIdentifier')->andReturn(Str::random(80));
$entity->shouldReceive('getUserIdentifier')->andReturn((string) $this->user->id);
$entity->shouldReceive('getClient')->andReturn($client);
$entity->shouldReceive('getScopes')->andReturn([]);
$entity->shouldReceive('getExpiryDateTime')->andReturn(now()->addMinutes(10)->toDateTimeImmutable());
Activation checklist + MCP OAuth authorize UX (#239) (#250) * Wire onboarding activation into Account, observers, and shared Inertia data Add onboarding casts/hasFinishedOnboarding, AccessToken ObservedBy, Platform::connectableOptions, Post/SocialAccount onboarding broadcast hooks, and lazy onboardingResidual share + SharedData types. * Register onboarding routes and post-checkout activation redirects. Wire billing processing and the sidebar checklist so owners land on activation after subscribe, with locale sidebar/uk onboarding strings. * Align MCP grant usability with onboarding activation checks Unbound MCP tokens fall back to the user's current workspace and require createPost so viewer/unscoped grants neither unlock the checklist nor broadcast onboarding status. * Require bound MCP workspace for onboarding activation. Drop current-workspace fallback from usable MCP grants so checklist detection and broadcasts match Passport token scoping; viewers still cannot unlock the MCP step. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden onboarding review findings and tighten locale strings. Fix Welcome/Persona/TrackPost suites broken by the activation route reuse and PostObserver analytics side effects, restore Echo poll fallbacks, reject unbound MCP grants in tests, and drop unused onboarding.mcp keys. Co-authored-by: Cursor <cursoragent@cursor.com> * Remove unused sidebar and MCP authorization locale keys. Drop dead sidebar menu/theme strings (including the overwritten workspace label and api_keys nav entry) and unused MCP authorize app_title/approving copy across all locales. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix SetLocale crashing on Passport Symfony OAuth responses. OAuth errors return a raw Symfony Response without withCookie(); attach the default locale cookie via headers so authorize no longer 500s. Co-authored-by: Cursor <cursoragent@cursor.com> * Prompt OAuth guests to log in before rejecting unknown clients. MCP Inspector often reuses a stale client_id; validateAuthorizationRequest was returning invalid_client JSON before the login redirect. Guests now hit /login first, then client validation runs after authentication. Co-authored-by: Cursor <cursoragent@cursor.com> * Render Inertia OAuth authorize errors for browser logins. After login, Inertia follows the intended authorize URL; raw invalid_client JSON broke that visit. HTML/Inertia requests now get mcp/AuthorizeError while API JSON clients still receive the OAuth error payload. Co-authored-by: Cursor <cursoragent@cursor.com> * Detect Inertia OAuth error pages via Request::inertia(). Use the framework helper so post-login authorize failures keep returning an Inertia page instead of raw OAuth JSON. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify OAuth authorize error page detection to expectsJson. Drop the X-Inertia header sniff; browser and Inertia visits already do not expectsJson, while API clients still receive the OAuth JSON payload. Co-authored-by: Cursor <cursoragent@cursor.com> * Share MCP authorize layout and drop the error close button. Keep authorize and authorize-error on the same centered card shell instead of the auth split layout. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding activation for reviewability and safety. Use an exists-based MCP check, keep GETs read-only, move sync into syncAndNotify, clear MCP skips on connect, restrict complete to owners, and share Echo/poll via one composable. Co-authored-by: Cursor <cursoragent@cursor.com> * Move MCP OAuth authorize UX out of the onboarding PR. Keep the activation checklist focused; OAuth guest/error-page work now lives on fix/mcp-oauth-authorize-ux. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix corrupted French MCP locale after OAuth key cleanup. Co-authored-by: Cursor <cursoragent@cursor.com> * Restore MCP OAuth authorize UX onto the onboarding branch. Keep authorize error page, guest login-before-client validation, and SetLocale Symfony cookie fix in #250. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix OAuth prompt=none redirects and harden onboarding tests. Keep login_required/consent_required as redirects instead of Inertia, add regression coverage for owner-only activation, require invite email confirmation, and align MCP connected apps with the sessions list UI. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding guards and dedupe viewed analytics. Introduce isOnboardingOpen / belongsToAccount helpers, collapse duplicated sync/dispatch paths, and capture onboarding.viewed once per account. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding event, observers, and status helpers. Tighten Account onboarding predicates, drop nullable broadcast/dispatch APIs, and collapse repeated observer/controller guards. Co-authored-by: Cursor <cursoragent@cursor.com> * Treat in-app users as always having an account. Add resolveAccount(), tighten belongsToAccount to string ids, and fold guest residual handling into ResolveOnboardingStatus. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename onboarding residual share test to progress. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding status and rename residual to progress. Use accountOrFail, extract MCP onboarding scope, auto-leave the ready screen, and send non-onboarding checkout back to accounts. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract HasAccount and prefer data_get in onboarding flows. Move account helpers off User, drop nullable sidebarProgress, and read OAuth/onboarding payloads with data_get. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding checks and extract HasOnboarding. Use Eloquent + policies for MCP/backfill paths, and move account onboarding helpers into a dedicated trait. Co-authored-by: Cursor <cursoragent@cursor.com> * Add trait tests and tidy onboarding imports. Cover HasAccount and HasOnboarding under Models/Traits, prefer filled() for checkout session ids, and import Throwable instead of FQCN. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify checkout session_id and OAuth error props. Read session_id via request->string(), and take OAuth error details from the League exception instead of decoding the response body. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify PostObserver onboarding notify path. Share one otherPosts check for first-create and last-delete instead of separate callbacks. Co-authored-by: Cursor <cursoragent@cursor.com> * Use post author as onboarding sync actor. Drop Auth::user() preference in PostObserver; checklist sync attributes to $post->user. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify SocialAccountObserver and OAuth authorize flow. Share create/delete onboarding notify, drop Auth actor fallback to owner, and inline Passport Inertia error handling. Co-authored-by: Cursor <cursoragent@cursor.com> * Use lazy Inertia props for onboarding partial reloads. Drop partial-header branching; wrap page props in closures and always redirect completed/dismissed accounts to the calendar. Co-authored-by: Cursor <cursoragent@cursor.com> * Defer sidebar onboarding progress and stamp completion as owner-only. Skip the MCP checklist work on full Inertia visits via deferred shared props, early-exit token scans, and keep account completion stamps owner-gated. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify deferred onboarding progress share via canShowProgress. Co-authored-by: Cursor <cursoragent@cursor.com> * Add User firstName for shared auth and simplify onboarding page. Co-authored-by: Cursor <cursoragent@cursor.com> * Move User firstName coverage into UserTest. Co-authored-by: Cursor <cursoragent@cursor.com> * Use first_name directly without empty-name fallbacks. Co-authored-by: Cursor <cursoragent@cursor.com> * Resolve onboarding sample prompt on the frontend via i18n. Co-authored-by: Cursor <cursoragent@cursor.com> * Stamp onboarding completion via the account owner after teammate unlocks. Co-authored-by: Cursor <cursoragent@cursor.com> * Count only the account owner MCP grant toward onboarding activation. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix OAuth consent auth-token mismatch for mid-activation owners. Skip deferred onboardingProgress on Passport authorize so Inertia does not rotate the session authToken, cover happy and stale-token paths in tests, and polish MCP setup copy plus sidebar/onboarding layout. Co-authored-by: Cursor <cursoragent@cursor.com> * Keep users on onboarding after activation completes. Stamp completion and re-render the finished checklist instead of redirecting to the calendar so owners can review the done state. Co-authored-by: Cursor <cursoragent@cursor.com> * Clarify Passport consent-view opt-out and guard app-route deferral. Rename the authorize-only route check and assert onboardingProgress still defers on calendar, onboarding, and MCP settings. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden onboarding completion and MCP consent workspace binding. Reject OAuth approve without a workspace, retry auto-complete until stamped, send dismissed complete straight to calendar, and cover the device consent defer opt-out. Co-authored-by: Cursor <cursoragent@cursor.com> * Enable activation checklist for self-hosted installs. Remove the self-hosted onboarding redirects, keep the SaaS-only dismiss backfill, and cover subscription-less owners plus skip/complete destinations. Co-authored-by: Cursor <cursoragent@cursor.com> * Add GitHub, Hacker News, and directories referral sources. Expand the welcome referral step with open-source and directory discovery channels. Co-authored-by: Cursor <cursoragent@cursor.com> * Refine welcome referral sources and labels. Split Instagram/Threads, add Founder, and shorten Google, GitHub, AI, and blog option labels. Co-authored-by: Cursor <cursoragent@cursor.com> * Sort accounts platforms alphabetically and drop connect hover plus. Reuse connectableOptions for the accounts index and remove the unused plus badge on disconnected cards. Co-authored-by: Cursor <cursoragent@cursor.com> * Centralize PostHog once-capture so disabled installs don't burn dedupe keys. Move isEnabled + Cache::add into PostHogService::captureOnce and route onboarding viewed/step events through it. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding backfill to complete every existing open account. Drop self-hosted and subscription filters; down clears completed_at again. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop PostHog captureOnce and use plain capture for onboarding. Remove cache-based event dedupe; callers rely on PostHogService::capture gating. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 23:34:43 +00:00
expect(fn () => app(AuthCodeRepository::class)->persistNewAuthCode($entity))
->toThrow(OAuthServerException::class);
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
Activation checklist + MCP OAuth authorize UX (#239) (#250) * Wire onboarding activation into Account, observers, and shared Inertia data Add onboarding casts/hasFinishedOnboarding, AccessToken ObservedBy, Platform::connectableOptions, Post/SocialAccount onboarding broadcast hooks, and lazy onboardingResidual share + SharedData types. * Register onboarding routes and post-checkout activation redirects. Wire billing processing and the sidebar checklist so owners land on activation after subscribe, with locale sidebar/uk onboarding strings. * Align MCP grant usability with onboarding activation checks Unbound MCP tokens fall back to the user's current workspace and require createPost so viewer/unscoped grants neither unlock the checklist nor broadcast onboarding status. * Require bound MCP workspace for onboarding activation. Drop current-workspace fallback from usable MCP grants so checklist detection and broadcasts match Passport token scoping; viewers still cannot unlock the MCP step. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden onboarding review findings and tighten locale strings. Fix Welcome/Persona/TrackPost suites broken by the activation route reuse and PostObserver analytics side effects, restore Echo poll fallbacks, reject unbound MCP grants in tests, and drop unused onboarding.mcp keys. Co-authored-by: Cursor <cursoragent@cursor.com> * Remove unused sidebar and MCP authorization locale keys. Drop dead sidebar menu/theme strings (including the overwritten workspace label and api_keys nav entry) and unused MCP authorize app_title/approving copy across all locales. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix SetLocale crashing on Passport Symfony OAuth responses. OAuth errors return a raw Symfony Response without withCookie(); attach the default locale cookie via headers so authorize no longer 500s. Co-authored-by: Cursor <cursoragent@cursor.com> * Prompt OAuth guests to log in before rejecting unknown clients. MCP Inspector often reuses a stale client_id; validateAuthorizationRequest was returning invalid_client JSON before the login redirect. Guests now hit /login first, then client validation runs after authentication. Co-authored-by: Cursor <cursoragent@cursor.com> * Render Inertia OAuth authorize errors for browser logins. After login, Inertia follows the intended authorize URL; raw invalid_client JSON broke that visit. HTML/Inertia requests now get mcp/AuthorizeError while API JSON clients still receive the OAuth error payload. Co-authored-by: Cursor <cursoragent@cursor.com> * Detect Inertia OAuth error pages via Request::inertia(). Use the framework helper so post-login authorize failures keep returning an Inertia page instead of raw OAuth JSON. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify OAuth authorize error page detection to expectsJson. Drop the X-Inertia header sniff; browser and Inertia visits already do not expectsJson, while API clients still receive the OAuth JSON payload. Co-authored-by: Cursor <cursoragent@cursor.com> * Share MCP authorize layout and drop the error close button. Keep authorize and authorize-error on the same centered card shell instead of the auth split layout. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding activation for reviewability and safety. Use an exists-based MCP check, keep GETs read-only, move sync into syncAndNotify, clear MCP skips on connect, restrict complete to owners, and share Echo/poll via one composable. Co-authored-by: Cursor <cursoragent@cursor.com> * Move MCP OAuth authorize UX out of the onboarding PR. Keep the activation checklist focused; OAuth guest/error-page work now lives on fix/mcp-oauth-authorize-ux. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix corrupted French MCP locale after OAuth key cleanup. Co-authored-by: Cursor <cursoragent@cursor.com> * Restore MCP OAuth authorize UX onto the onboarding branch. Keep authorize error page, guest login-before-client validation, and SetLocale Symfony cookie fix in #250. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix OAuth prompt=none redirects and harden onboarding tests. Keep login_required/consent_required as redirects instead of Inertia, add regression coverage for owner-only activation, require invite email confirmation, and align MCP connected apps with the sessions list UI. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding guards and dedupe viewed analytics. Introduce isOnboardingOpen / belongsToAccount helpers, collapse duplicated sync/dispatch paths, and capture onboarding.viewed once per account. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding event, observers, and status helpers. Tighten Account onboarding predicates, drop nullable broadcast/dispatch APIs, and collapse repeated observer/controller guards. Co-authored-by: Cursor <cursoragent@cursor.com> * Treat in-app users as always having an account. Add resolveAccount(), tighten belongsToAccount to string ids, and fold guest residual handling into ResolveOnboardingStatus. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename onboarding residual share test to progress. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding status and rename residual to progress. Use accountOrFail, extract MCP onboarding scope, auto-leave the ready screen, and send non-onboarding checkout back to accounts. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract HasAccount and prefer data_get in onboarding flows. Move account helpers off User, drop nullable sidebarProgress, and read OAuth/onboarding payloads with data_get. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding checks and extract HasOnboarding. Use Eloquent + policies for MCP/backfill paths, and move account onboarding helpers into a dedicated trait. Co-authored-by: Cursor <cursoragent@cursor.com> * Add trait tests and tidy onboarding imports. Cover HasAccount and HasOnboarding under Models/Traits, prefer filled() for checkout session ids, and import Throwable instead of FQCN. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify checkout session_id and OAuth error props. Read session_id via request->string(), and take OAuth error details from the League exception instead of decoding the response body. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify PostObserver onboarding notify path. Share one otherPosts check for first-create and last-delete instead of separate callbacks. Co-authored-by: Cursor <cursoragent@cursor.com> * Use post author as onboarding sync actor. Drop Auth::user() preference in PostObserver; checklist sync attributes to $post->user. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify SocialAccountObserver and OAuth authorize flow. Share create/delete onboarding notify, drop Auth actor fallback to owner, and inline Passport Inertia error handling. Co-authored-by: Cursor <cursoragent@cursor.com> * Use lazy Inertia props for onboarding partial reloads. Drop partial-header branching; wrap page props in closures and always redirect completed/dismissed accounts to the calendar. Co-authored-by: Cursor <cursoragent@cursor.com> * Defer sidebar onboarding progress and stamp completion as owner-only. Skip the MCP checklist work on full Inertia visits via deferred shared props, early-exit token scans, and keep account completion stamps owner-gated. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify deferred onboarding progress share via canShowProgress. Co-authored-by: Cursor <cursoragent@cursor.com> * Add User firstName for shared auth and simplify onboarding page. Co-authored-by: Cursor <cursoragent@cursor.com> * Move User firstName coverage into UserTest. Co-authored-by: Cursor <cursoragent@cursor.com> * Use first_name directly without empty-name fallbacks. Co-authored-by: Cursor <cursoragent@cursor.com> * Resolve onboarding sample prompt on the frontend via i18n. Co-authored-by: Cursor <cursoragent@cursor.com> * Stamp onboarding completion via the account owner after teammate unlocks. Co-authored-by: Cursor <cursoragent@cursor.com> * Count only the account owner MCP grant toward onboarding activation. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix OAuth consent auth-token mismatch for mid-activation owners. Skip deferred onboardingProgress on Passport authorize so Inertia does not rotate the session authToken, cover happy and stale-token paths in tests, and polish MCP setup copy plus sidebar/onboarding layout. Co-authored-by: Cursor <cursoragent@cursor.com> * Keep users on onboarding after activation completes. Stamp completion and re-render the finished checklist instead of redirecting to the calendar so owners can review the done state. Co-authored-by: Cursor <cursoragent@cursor.com> * Clarify Passport consent-view opt-out and guard app-route deferral. Rename the authorize-only route check and assert onboardingProgress still defers on calendar, onboarding, and MCP settings. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden onboarding completion and MCP consent workspace binding. Reject OAuth approve without a workspace, retry auto-complete until stamped, send dismissed complete straight to calendar, and cover the device consent defer opt-out. Co-authored-by: Cursor <cursoragent@cursor.com> * Enable activation checklist for self-hosted installs. Remove the self-hosted onboarding redirects, keep the SaaS-only dismiss backfill, and cover subscription-less owners plus skip/complete destinations. Co-authored-by: Cursor <cursoragent@cursor.com> * Add GitHub, Hacker News, and directories referral sources. Expand the welcome referral step with open-source and directory discovery channels. Co-authored-by: Cursor <cursoragent@cursor.com> * Refine welcome referral sources and labels. Split Instagram/Threads, add Founder, and shorten Google, GitHub, AI, and blog option labels. Co-authored-by: Cursor <cursoragent@cursor.com> * Sort accounts platforms alphabetically and drop connect hover plus. Reuse connectableOptions for the accounts index and remove the unused plus badge on disconnected cards. Co-authored-by: Cursor <cursoragent@cursor.com> * Centralize PostHog once-capture so disabled installs don't burn dedupe keys. Move isEnabled + Cache::add into PostHogService::captureOnce and route onboarding viewed/step events through it. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding backfill to complete every existing open account. Drop self-hosted and subscription filters; down clears completed_at again. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop PostHog captureOnce and use plain capture for onboarding. Remove cache-based event dedupe; callers rely on PostHogService::capture gating. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 23:34:43 +00:00
expect(AuthCode::query()->where('client_id', $this->clientId)->exists())->toBeFalse();
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
});
test('auth code repository rejects a consent workspace the user no longer belongs to', function () {
$this->workspace->members()->detach($this->user->id);
$this->actingAs($this->user->fresh());
request()->merge(['workspace_id' => $this->workspace->id]);
$client = Mockery::mock(ClientEntityInterface::class);
$client->shouldReceive('getIdentifier')->andReturn($this->clientId);
$entity = Mockery::mock(AuthCodeEntityInterface::class);
$entity->shouldReceive('getIdentifier')->andReturn(Str::random(80));
$entity->shouldReceive('getUserIdentifier')->andReturn((string) $this->user->id);
$entity->shouldReceive('getClient')->andReturn($client);
$entity->shouldReceive('getScopes')->andReturn([]);
$entity->shouldReceive('getExpiryDateTime')->andReturn(now()->addMinutes(10)->toDateTimeImmutable());
Activation checklist + MCP OAuth authorize UX (#239) (#250) * Wire onboarding activation into Account, observers, and shared Inertia data Add onboarding casts/hasFinishedOnboarding, AccessToken ObservedBy, Platform::connectableOptions, Post/SocialAccount onboarding broadcast hooks, and lazy onboardingResidual share + SharedData types. * Register onboarding routes and post-checkout activation redirects. Wire billing processing and the sidebar checklist so owners land on activation after subscribe, with locale sidebar/uk onboarding strings. * Align MCP grant usability with onboarding activation checks Unbound MCP tokens fall back to the user's current workspace and require createPost so viewer/unscoped grants neither unlock the checklist nor broadcast onboarding status. * Require bound MCP workspace for onboarding activation. Drop current-workspace fallback from usable MCP grants so checklist detection and broadcasts match Passport token scoping; viewers still cannot unlock the MCP step. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden onboarding review findings and tighten locale strings. Fix Welcome/Persona/TrackPost suites broken by the activation route reuse and PostObserver analytics side effects, restore Echo poll fallbacks, reject unbound MCP grants in tests, and drop unused onboarding.mcp keys. Co-authored-by: Cursor <cursoragent@cursor.com> * Remove unused sidebar and MCP authorization locale keys. Drop dead sidebar menu/theme strings (including the overwritten workspace label and api_keys nav entry) and unused MCP authorize app_title/approving copy across all locales. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix SetLocale crashing on Passport Symfony OAuth responses. OAuth errors return a raw Symfony Response without withCookie(); attach the default locale cookie via headers so authorize no longer 500s. Co-authored-by: Cursor <cursoragent@cursor.com> * Prompt OAuth guests to log in before rejecting unknown clients. MCP Inspector often reuses a stale client_id; validateAuthorizationRequest was returning invalid_client JSON before the login redirect. Guests now hit /login first, then client validation runs after authentication. Co-authored-by: Cursor <cursoragent@cursor.com> * Render Inertia OAuth authorize errors for browser logins. After login, Inertia follows the intended authorize URL; raw invalid_client JSON broke that visit. HTML/Inertia requests now get mcp/AuthorizeError while API JSON clients still receive the OAuth error payload. Co-authored-by: Cursor <cursoragent@cursor.com> * Detect Inertia OAuth error pages via Request::inertia(). Use the framework helper so post-login authorize failures keep returning an Inertia page instead of raw OAuth JSON. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify OAuth authorize error page detection to expectsJson. Drop the X-Inertia header sniff; browser and Inertia visits already do not expectsJson, while API clients still receive the OAuth JSON payload. Co-authored-by: Cursor <cursoragent@cursor.com> * Share MCP authorize layout and drop the error close button. Keep authorize and authorize-error on the same centered card shell instead of the auth split layout. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding activation for reviewability and safety. Use an exists-based MCP check, keep GETs read-only, move sync into syncAndNotify, clear MCP skips on connect, restrict complete to owners, and share Echo/poll via one composable. Co-authored-by: Cursor <cursoragent@cursor.com> * Move MCP OAuth authorize UX out of the onboarding PR. Keep the activation checklist focused; OAuth guest/error-page work now lives on fix/mcp-oauth-authorize-ux. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix corrupted French MCP locale after OAuth key cleanup. Co-authored-by: Cursor <cursoragent@cursor.com> * Restore MCP OAuth authorize UX onto the onboarding branch. Keep authorize error page, guest login-before-client validation, and SetLocale Symfony cookie fix in #250. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix OAuth prompt=none redirects and harden onboarding tests. Keep login_required/consent_required as redirects instead of Inertia, add regression coverage for owner-only activation, require invite email confirmation, and align MCP connected apps with the sessions list UI. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding guards and dedupe viewed analytics. Introduce isOnboardingOpen / belongsToAccount helpers, collapse duplicated sync/dispatch paths, and capture onboarding.viewed once per account. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding event, observers, and status helpers. Tighten Account onboarding predicates, drop nullable broadcast/dispatch APIs, and collapse repeated observer/controller guards. Co-authored-by: Cursor <cursoragent@cursor.com> * Treat in-app users as always having an account. Add resolveAccount(), tighten belongsToAccount to string ids, and fold guest residual handling into ResolveOnboardingStatus. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename onboarding residual share test to progress. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding status and rename residual to progress. Use accountOrFail, extract MCP onboarding scope, auto-leave the ready screen, and send non-onboarding checkout back to accounts. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract HasAccount and prefer data_get in onboarding flows. Move account helpers off User, drop nullable sidebarProgress, and read OAuth/onboarding payloads with data_get. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding checks and extract HasOnboarding. Use Eloquent + policies for MCP/backfill paths, and move account onboarding helpers into a dedicated trait. Co-authored-by: Cursor <cursoragent@cursor.com> * Add trait tests and tidy onboarding imports. Cover HasAccount and HasOnboarding under Models/Traits, prefer filled() for checkout session ids, and import Throwable instead of FQCN. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify checkout session_id and OAuth error props. Read session_id via request->string(), and take OAuth error details from the League exception instead of decoding the response body. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify PostObserver onboarding notify path. Share one otherPosts check for first-create and last-delete instead of separate callbacks. Co-authored-by: Cursor <cursoragent@cursor.com> * Use post author as onboarding sync actor. Drop Auth::user() preference in PostObserver; checklist sync attributes to $post->user. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify SocialAccountObserver and OAuth authorize flow. Share create/delete onboarding notify, drop Auth actor fallback to owner, and inline Passport Inertia error handling. Co-authored-by: Cursor <cursoragent@cursor.com> * Use lazy Inertia props for onboarding partial reloads. Drop partial-header branching; wrap page props in closures and always redirect completed/dismissed accounts to the calendar. Co-authored-by: Cursor <cursoragent@cursor.com> * Defer sidebar onboarding progress and stamp completion as owner-only. Skip the MCP checklist work on full Inertia visits via deferred shared props, early-exit token scans, and keep account completion stamps owner-gated. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify deferred onboarding progress share via canShowProgress. Co-authored-by: Cursor <cursoragent@cursor.com> * Add User firstName for shared auth and simplify onboarding page. Co-authored-by: Cursor <cursoragent@cursor.com> * Move User firstName coverage into UserTest. Co-authored-by: Cursor <cursoragent@cursor.com> * Use first_name directly without empty-name fallbacks. Co-authored-by: Cursor <cursoragent@cursor.com> * Resolve onboarding sample prompt on the frontend via i18n. Co-authored-by: Cursor <cursoragent@cursor.com> * Stamp onboarding completion via the account owner after teammate unlocks. Co-authored-by: Cursor <cursoragent@cursor.com> * Count only the account owner MCP grant toward onboarding activation. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix OAuth consent auth-token mismatch for mid-activation owners. Skip deferred onboardingProgress on Passport authorize so Inertia does not rotate the session authToken, cover happy and stale-token paths in tests, and polish MCP setup copy plus sidebar/onboarding layout. Co-authored-by: Cursor <cursoragent@cursor.com> * Keep users on onboarding after activation completes. Stamp completion and re-render the finished checklist instead of redirecting to the calendar so owners can review the done state. Co-authored-by: Cursor <cursoragent@cursor.com> * Clarify Passport consent-view opt-out and guard app-route deferral. Rename the authorize-only route check and assert onboardingProgress still defers on calendar, onboarding, and MCP settings. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden onboarding completion and MCP consent workspace binding. Reject OAuth approve without a workspace, retry auto-complete until stamped, send dismissed complete straight to calendar, and cover the device consent defer opt-out. Co-authored-by: Cursor <cursoragent@cursor.com> * Enable activation checklist for self-hosted installs. Remove the self-hosted onboarding redirects, keep the SaaS-only dismiss backfill, and cover subscription-less owners plus skip/complete destinations. Co-authored-by: Cursor <cursoragent@cursor.com> * Add GitHub, Hacker News, and directories referral sources. Expand the welcome referral step with open-source and directory discovery channels. Co-authored-by: Cursor <cursoragent@cursor.com> * Refine welcome referral sources and labels. Split Instagram/Threads, add Founder, and shorten Google, GitHub, AI, and blog option labels. Co-authored-by: Cursor <cursoragent@cursor.com> * Sort accounts platforms alphabetically and drop connect hover plus. Reuse connectableOptions for the accounts index and remove the unused plus badge on disconnected cards. Co-authored-by: Cursor <cursoragent@cursor.com> * Centralize PostHog once-capture so disabled installs don't burn dedupe keys. Move isEnabled + Cache::add into PostHogService::captureOnce and route onboarding viewed/step events through it. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding backfill to complete every existing open account. Drop self-hosted and subscription filters; down clears completed_at again. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop PostHog captureOnce and use plain capture for onboarding. Remove cache-based event dedupe; callers rely on PostHogService::capture gating. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 23:34:43 +00:00
expect(fn () => app(AuthCodeRepository::class)->persistNewAuthCode($entity))
->toThrow(OAuthServerException::class);
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
Activation checklist + MCP OAuth authorize UX (#239) (#250) * Wire onboarding activation into Account, observers, and shared Inertia data Add onboarding casts/hasFinishedOnboarding, AccessToken ObservedBy, Platform::connectableOptions, Post/SocialAccount onboarding broadcast hooks, and lazy onboardingResidual share + SharedData types. * Register onboarding routes and post-checkout activation redirects. Wire billing processing and the sidebar checklist so owners land on activation after subscribe, with locale sidebar/uk onboarding strings. * Align MCP grant usability with onboarding activation checks Unbound MCP tokens fall back to the user's current workspace and require createPost so viewer/unscoped grants neither unlock the checklist nor broadcast onboarding status. * Require bound MCP workspace for onboarding activation. Drop current-workspace fallback from usable MCP grants so checklist detection and broadcasts match Passport token scoping; viewers still cannot unlock the MCP step. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden onboarding review findings and tighten locale strings. Fix Welcome/Persona/TrackPost suites broken by the activation route reuse and PostObserver analytics side effects, restore Echo poll fallbacks, reject unbound MCP grants in tests, and drop unused onboarding.mcp keys. Co-authored-by: Cursor <cursoragent@cursor.com> * Remove unused sidebar and MCP authorization locale keys. Drop dead sidebar menu/theme strings (including the overwritten workspace label and api_keys nav entry) and unused MCP authorize app_title/approving copy across all locales. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix SetLocale crashing on Passport Symfony OAuth responses. OAuth errors return a raw Symfony Response without withCookie(); attach the default locale cookie via headers so authorize no longer 500s. Co-authored-by: Cursor <cursoragent@cursor.com> * Prompt OAuth guests to log in before rejecting unknown clients. MCP Inspector often reuses a stale client_id; validateAuthorizationRequest was returning invalid_client JSON before the login redirect. Guests now hit /login first, then client validation runs after authentication. Co-authored-by: Cursor <cursoragent@cursor.com> * Render Inertia OAuth authorize errors for browser logins. After login, Inertia follows the intended authorize URL; raw invalid_client JSON broke that visit. HTML/Inertia requests now get mcp/AuthorizeError while API JSON clients still receive the OAuth error payload. Co-authored-by: Cursor <cursoragent@cursor.com> * Detect Inertia OAuth error pages via Request::inertia(). Use the framework helper so post-login authorize failures keep returning an Inertia page instead of raw OAuth JSON. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify OAuth authorize error page detection to expectsJson. Drop the X-Inertia header sniff; browser and Inertia visits already do not expectsJson, while API clients still receive the OAuth JSON payload. Co-authored-by: Cursor <cursoragent@cursor.com> * Share MCP authorize layout and drop the error close button. Keep authorize and authorize-error on the same centered card shell instead of the auth split layout. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding activation for reviewability and safety. Use an exists-based MCP check, keep GETs read-only, move sync into syncAndNotify, clear MCP skips on connect, restrict complete to owners, and share Echo/poll via one composable. Co-authored-by: Cursor <cursoragent@cursor.com> * Move MCP OAuth authorize UX out of the onboarding PR. Keep the activation checklist focused; OAuth guest/error-page work now lives on fix/mcp-oauth-authorize-ux. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix corrupted French MCP locale after OAuth key cleanup. Co-authored-by: Cursor <cursoragent@cursor.com> * Restore MCP OAuth authorize UX onto the onboarding branch. Keep authorize error page, guest login-before-client validation, and SetLocale Symfony cookie fix in #250. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix OAuth prompt=none redirects and harden onboarding tests. Keep login_required/consent_required as redirects instead of Inertia, add regression coverage for owner-only activation, require invite email confirmation, and align MCP connected apps with the sessions list UI. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding guards and dedupe viewed analytics. Introduce isOnboardingOpen / belongsToAccount helpers, collapse duplicated sync/dispatch paths, and capture onboarding.viewed once per account. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding event, observers, and status helpers. Tighten Account onboarding predicates, drop nullable broadcast/dispatch APIs, and collapse repeated observer/controller guards. Co-authored-by: Cursor <cursoragent@cursor.com> * Treat in-app users as always having an account. Add resolveAccount(), tighten belongsToAccount to string ids, and fold guest residual handling into ResolveOnboardingStatus. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename onboarding residual share test to progress. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding status and rename residual to progress. Use accountOrFail, extract MCP onboarding scope, auto-leave the ready screen, and send non-onboarding checkout back to accounts. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract HasAccount and prefer data_get in onboarding flows. Move account helpers off User, drop nullable sidebarProgress, and read OAuth/onboarding payloads with data_get. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding checks and extract HasOnboarding. Use Eloquent + policies for MCP/backfill paths, and move account onboarding helpers into a dedicated trait. Co-authored-by: Cursor <cursoragent@cursor.com> * Add trait tests and tidy onboarding imports. Cover HasAccount and HasOnboarding under Models/Traits, prefer filled() for checkout session ids, and import Throwable instead of FQCN. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify checkout session_id and OAuth error props. Read session_id via request->string(), and take OAuth error details from the League exception instead of decoding the response body. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify PostObserver onboarding notify path. Share one otherPosts check for first-create and last-delete instead of separate callbacks. Co-authored-by: Cursor <cursoragent@cursor.com> * Use post author as onboarding sync actor. Drop Auth::user() preference in PostObserver; checklist sync attributes to $post->user. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify SocialAccountObserver and OAuth authorize flow. Share create/delete onboarding notify, drop Auth actor fallback to owner, and inline Passport Inertia error handling. Co-authored-by: Cursor <cursoragent@cursor.com> * Use lazy Inertia props for onboarding partial reloads. Drop partial-header branching; wrap page props in closures and always redirect completed/dismissed accounts to the calendar. Co-authored-by: Cursor <cursoragent@cursor.com> * Defer sidebar onboarding progress and stamp completion as owner-only. Skip the MCP checklist work on full Inertia visits via deferred shared props, early-exit token scans, and keep account completion stamps owner-gated. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify deferred onboarding progress share via canShowProgress. Co-authored-by: Cursor <cursoragent@cursor.com> * Add User firstName for shared auth and simplify onboarding page. Co-authored-by: Cursor <cursoragent@cursor.com> * Move User firstName coverage into UserTest. Co-authored-by: Cursor <cursoragent@cursor.com> * Use first_name directly without empty-name fallbacks. Co-authored-by: Cursor <cursoragent@cursor.com> * Resolve onboarding sample prompt on the frontend via i18n. Co-authored-by: Cursor <cursoragent@cursor.com> * Stamp onboarding completion via the account owner after teammate unlocks. Co-authored-by: Cursor <cursoragent@cursor.com> * Count only the account owner MCP grant toward onboarding activation. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix OAuth consent auth-token mismatch for mid-activation owners. Skip deferred onboardingProgress on Passport authorize so Inertia does not rotate the session authToken, cover happy and stale-token paths in tests, and polish MCP setup copy plus sidebar/onboarding layout. Co-authored-by: Cursor <cursoragent@cursor.com> * Keep users on onboarding after activation completes. Stamp completion and re-render the finished checklist instead of redirecting to the calendar so owners can review the done state. Co-authored-by: Cursor <cursoragent@cursor.com> * Clarify Passport consent-view opt-out and guard app-route deferral. Rename the authorize-only route check and assert onboardingProgress still defers on calendar, onboarding, and MCP settings. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden onboarding completion and MCP consent workspace binding. Reject OAuth approve without a workspace, retry auto-complete until stamped, send dismissed complete straight to calendar, and cover the device consent defer opt-out. Co-authored-by: Cursor <cursoragent@cursor.com> * Enable activation checklist for self-hosted installs. Remove the self-hosted onboarding redirects, keep the SaaS-only dismiss backfill, and cover subscription-less owners plus skip/complete destinations. Co-authored-by: Cursor <cursoragent@cursor.com> * Add GitHub, Hacker News, and directories referral sources. Expand the welcome referral step with open-source and directory discovery channels. Co-authored-by: Cursor <cursoragent@cursor.com> * Refine welcome referral sources and labels. Split Instagram/Threads, add Founder, and shorten Google, GitHub, AI, and blog option labels. Co-authored-by: Cursor <cursoragent@cursor.com> * Sort accounts platforms alphabetically and drop connect hover plus. Reuse connectableOptions for the accounts index and remove the unused plus badge on disconnected cards. Co-authored-by: Cursor <cursoragent@cursor.com> * Centralize PostHog once-capture so disabled installs don't burn dedupe keys. Move isEnabled + Cache::add into PostHogService::captureOnce and route onboarding viewed/step events through it. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify onboarding backfill to complete every existing open account. Drop self-hosted and subscription filters; down clears completed_at again. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop PostHog captureOnce and use plain capture for onboarding. Remove cache-based event dedupe; callers rely on PostHogService::capture gating. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 23:34:43 +00:00
expect(AuthCode::query()->where('client_id', $this->clientId)->exists())->toBeFalse();
Scope MCP OAuth tokens to user + workspace (#222) (#245) * Scope MCP OAuth tokens to user + workspace Bind authorization-code grants to the authorizing workspace (via auth codes), inherit workspace on refresh, resolve MCP/API requests from the token instead of current_workspace_id, backfill existing grants, and revoke workspace tokens when a member is removed. Co-authored-by: Cursor <cursoragent@cursor.com> * Add multi-workspace MCP OAuth coverage Cover coexistence of the same client across workspaces, settings list/disconnect scoped to the current workspace, and API key controllers excluding workspace-bound MCP grants. Co-authored-by: Cursor <cursoragent@cursor.com> * Use constrained foreignUuid for oauth_auth_codes.workspace_id Match the project's UUID foreign-key convention instead of a separate foreign() call. Co-authored-by: Cursor <cursoragent@cursor.com> * Localize the MCP OAuth authorize consent screen Wire authorize.blade.php to mcp.* translation keys (including the workspace scope copy) and cover pt-BR rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix invalid Mockery import in bind workspace test CI treats the non-compound `use Mockery` as an ErrorException and aborts the whole parallel suite. Co-authored-by: Cursor <cursoragent@cursor.com> * Inline MCP OAuth workspace backfill into the migration Move the one-shot backfill out of a dedicated Action and wrap it in an explicit transaction so a failure rolls back partial binds/revokes. Co-authored-by: Cursor <cursoragent@cursor.com> * Nest MCP authorize i18n keys and test backfill rollback Group consent-screen copy under mcp.authorize.*, and assert the workspace backfill migration rolls back binds when it fails before commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Hardcode TryPost in the MCP authorize page title Drop the config('app.name') interpolation from the consent screen title. Co-authored-by: Cursor <cursoragent@cursor.com> * Add workspace picker to MCP OAuth consent screen Let users choose which workspace to bind at authorize time instead of always using current_workspace_id; silent re-consent still falls back. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP authorize workspace select spacing Match NativeSelect styling and give the label, control, and helper text room to breathe. Co-authored-by: Cursor <cursoragent@cursor.com> * Convert MCP OAuth consent screen to Inertia Vue Reuse AuthCardLayout, Button, and NativeSelect so the authorize page matches the app UI. Keep native form posts so Passport's external redirect still works for MCP client popups. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish MCP authorize layout with logo and workspace combobox Drop the shield and AuthCardLayout double-logo, put TryPost branding at the top, and reuse the app Combobox pattern for workspace search. Co-authored-by: Cursor <cursoragent@cursor.com> * Align MCP OAuth workspace backfill with mcpOAuth scope Reuse AccessToken::mcpOAuth() so the migration only touches mcp:use grants on non-PAT clients, matching the rest of the codebase. Co-authored-by: Cursor <cursoragent@cursor.com> * Tighten MCP OAuth workspace backfill heuristics Only touch connected MCP sessions, bind a sole membership or a valid current workspace, and revoke ambiguous multi-workspace grants instead of guessing the oldest workspace. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop Passport connection override from auth code migration Always use the app default database connection from .env. Co-authored-by: Cursor <cursoragent@cursor.com> * Bind MCP OAuth workspace in AccessTokenRepository Replace the AccessTokenCreated listener with the same Passport repository override pattern used for auth codes, so workspace_id is set at persist. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AccessTokenRepository workspace binding Drop redundant string casts and the oldest-workspace fallback; keep a small ownedWorkspace/payloadId helper surface instead. Co-authored-by: Cursor <cursoragent@cursor.com> * Extract Passport MCP authorization view from AppServiceProvider Keep configurePassport thin by moving the Inertia consent props into an invokable App\Passport\AuthorizationView class. Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify AuthorizationView and cover it with direct tests Use collection higher-order mapping for workspaces/scopes and add focused tests for current-workspace selection and empty-user props. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename BindWorkspaceToAccessTokenTest after listener removal The suite now covers AuthCodeRepository and AccessTokenRepository workspace binding, not an AccessTokenCreated listener. * Fail closed when auth code has no bindable workspace Authorization-code grants no longer fall back to the user's current workspace, so a token cannot be minted for a different tenant than consent. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after GitHub Actions infrastructure failures Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden MCP OAuth workspace binding on refresh and backfill Co-authored-by: Cursor <cursoragent@cursor.com> * fix: always show MCP OAuth consent to pick a workspace Disable Passport silent re-consent and require an explicit workspace_id from the consent form, with Passport wiring moved to its own provider. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: sort MCP connected clients by last used Show most recently used OAuth connections first on the workspace MCP settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:59:34 +00:00
});
test('oauth grant without a resolvable workspace fails before the token is saved', function () {
$this->user->update(['current_workspace_id' => null]);
$this->workspace->members()->detach($this->user->id);
$tokenId = Str::random(80);
request()->merge(['grant_type' => 'authorization_code']);
expect(fn () => persistAccessTokenEntity($tokenId, (string) $this->user->id, $this->clientId))
->toThrow(OAuthServerException::class);
expect(AccessToken::query()->find($tokenId))->toBeNull();
});
test('authorization code grant does not fall back to the users current workspace', function () {
$authCodeId = Str::random(80);
$tokenId = Str::random(80);
AuthCode::query()->forceCreate([
'id' => $authCodeId,
'user_id' => $this->user->id,
'client_id' => $this->clientId,
'workspace_id' => null,
'scopes' => '[]',
'revoked' => true,
'expires_at' => now()->addMinutes(10),
]);
request()->merge([
'grant_type' => 'authorization_code',
'code' => $this->decryptor->encrypt([
'client_id' => $this->clientId,
'auth_code_id' => $authCodeId,
'user_id' => (string) $this->user->id,
'scopes' => [],
'expire_time' => now()->addMinutes(10)->timestamp,
]),
]);
expect(fn () => persistAccessTokenEntity($tokenId, (string) $this->user->id, $this->clientId))
->toThrow(OAuthServerException::class);
expect(AccessToken::query()->find($tokenId))->toBeNull();
});
function persistAccessTokenEntity(string $tokenId, string $userId, string $clientId): void
{
$client = Mockery::mock(ClientEntityInterface::class);
$client->shouldReceive('getIdentifier')->andReturn($clientId);
$entity = Mockery::mock(AccessTokenEntityInterface::class);
$entity->shouldReceive('getIdentifier')->andReturn($tokenId);
$entity->shouldReceive('getUserIdentifier')->andReturn($userId);
$entity->shouldReceive('getClient')->andReturn($client);
$entity->shouldReceive('getScopes')->andReturn([]);
$entity->shouldReceive('getExpiryDateTime')->andReturn(now()->addHour()->toDateTimeImmutable());
app(AccessTokenRepository::class)->persistNewAccessToken($entity);
}