@@ -45,7 +45,11 @@ import {
4545import * as providerTransport from '@/lib/core/security/input-validation.server'
4646import { credentialGroupScopePolicyVersion } from '@/lib/credential-groups/provider-adapter'
4747import { getCredentialGroupProviderAdapter } from '@/lib/credential-groups/provider-registry'
48- import { encryptManagedOAuthTokenSet } from '@/lib/credentials/managed-oauth'
48+ import {
49+ decryptManagedOAuthTokenSet ,
50+ encryptManagedOAuthTokenSet ,
51+ rejectManagedOAuthToken ,
52+ } from '@/lib/credentials/managed-oauth'
4953import {
5054 claimToolExecution ,
5155 recordToolPermissionDecision ,
@@ -1113,7 +1117,7 @@ describe('Assistant Google Docs delivery', () => {
11131117} )
11141118
11151119describe ( 'Assistant writes cannot escape their admitted invocation' , ( ) => {
1116- it . each ( [ 'missing' , 'changed' , 'consumed' ] as const ) (
1120+ it . each ( [ 'missing' , 'workspace' , 'unscoped' , ' changed', 'consumed' ] as const ) (
11171121 'refuses %s execution authority before reaching the provider' ,
11181122 async ( attempt ) => {
11191123 const provider = vi . spyOn ( globalThis , 'fetch' ) . mockImplementation ( async ( ) => {
@@ -1130,7 +1134,11 @@ describe('Assistant writes cannot escape their admitted invocation', () => {
11301134 const context = {
11311135 userId : ownerId ,
11321136 workflowId : '' ,
1133- organizationId,
1137+ ...( attempt === 'workspace'
1138+ ? { workspaceId : generateId ( ) }
1139+ : attempt === 'unscoped'
1140+ ? { }
1141+ : { organizationId } ) ,
11341142 chatId,
11351143 toolCallId,
11361144 requestMode : 'assistant' as const ,
@@ -1143,7 +1151,7 @@ describe('Assistant writes cannot escape their admitted invocation', () => {
11431151 { operationContext : context }
11441152 )
11451153 try {
1146- if ( attempt === 'missing' ) {
1154+ if ( attempt === 'missing' || attempt === 'workspace' || attempt === 'unscoped' ) {
11471155 expect ( await execute ( ) ) . toMatchObject ( {
11481156 success : false ,
11491157 error : expect . stringContaining ( 'approved, durable' ) ,
@@ -1317,6 +1325,70 @@ describe('Assistant Jira sprint delivery', () => {
13171325} )
13181326
13191327describe ( 'Assistant Salesforce reviewed updates' , ( ) => {
1328+ it ( 'preserves a refreshed grant during rejection without resolving its API destination' , async ( ) => {
1329+ const [ saved ] = await db
1330+ . select ( {
1331+ encryptedOauthTokenSet : credential . encryptedOauthTokenSet ,
1332+ accessTokenExpiresAt : credential . accessTokenExpiresAt ,
1333+ refreshTokenExpiresAt : credential . refreshTokenExpiresAt ,
1334+ lastRefreshedAt : credential . lastRefreshedAt ,
1335+ managedOauthStatus : credential . managedOauthStatus ,
1336+ updatedAt : credential . updatedAt ,
1337+ } )
1338+ . from ( credential )
1339+ . where ( eq ( credential . id , salesforceCredentialId ) )
1340+ if ( ! saved ?. encryptedOauthTokenSet ) throw new Error ( 'Expected a saved Salesforce grant' )
1341+ const policy = await getCredentialGroupProviderAdapter ( 'salesforce' ) . getPolicy ( undefined , { } )
1342+ const requests : string [ ] = [ ]
1343+ const originalFetch = globalThis . fetch
1344+ const provider = vi . spyOn ( globalThis , 'fetch' ) . mockImplementation ( async ( input , init ) => {
1345+ const request = new Request ( input , init )
1346+ const url = new URL ( request . url )
1347+ if ( url . hostname !== 'login.salesforce.com' ) return originalFetch ( input , init )
1348+ requests . push ( url . pathname )
1349+ if ( url . pathname === '/services/oauth2/token' ) {
1350+ expect ( request . method ) . toBe ( 'POST' )
1351+ expect ( new URLSearchParams ( await request . text ( ) ) . get ( 'refresh_token' ) ) . toBe (
1352+ 'assistant-salesforce-refresh'
1353+ )
1354+ return Response . json ( {
1355+ access_token : 'refreshed-salesforce-token' ,
1356+ refresh_token : 'rotated-salesforce-refresh' ,
1357+ expires_in : 3600 ,
1358+ token_type : 'Bearer' ,
1359+ } )
1360+ }
1361+ return new Response ( null , { status : 503 } )
1362+ } )
1363+ try {
1364+ await expect (
1365+ rejectManagedOAuthToken ( {
1366+ organizationId,
1367+ credentialId : salesforceCredentialId ,
1368+ expectedProviderId : 'salesforce' ,
1369+ requiredScopes : policy . requiredScopes ,
1370+ rejectedAccessToken : 'assistant-salesforce-token' ,
1371+ } )
1372+ ) . resolves . toBe ( false )
1373+ expect ( requests ) . toEqual ( [ '/services/oauth2/token' ] )
1374+ const [ current ] = await db
1375+ . select ( )
1376+ . from ( credential )
1377+ . where ( eq ( credential . id , salesforceCredentialId ) )
1378+ if ( ! current ?. encryptedOauthTokenSet ) throw new Error ( 'Expected the refreshed grant' )
1379+ expect ( current . managedOauthStatus ) . toBe ( 'active' )
1380+ expect ( current . lastRefreshedAt ) . toBeInstanceOf ( Date )
1381+ expect ( current . accessTokenExpiresAt ?. getTime ( ) ) . toBeGreaterThan ( Date . now ( ) )
1382+ expect ( await decryptManagedOAuthTokenSet ( current . encryptedOauthTokenSet ) ) . toMatchObject ( {
1383+ accessToken : 'refreshed-salesforce-token' ,
1384+ refreshToken : 'rotated-salesforce-refresh' ,
1385+ } )
1386+ } finally {
1387+ provider . mockRestore ( )
1388+ await db . update ( credential ) . set ( saved ) . where ( eq ( credential . id , salesforceCredentialId ) )
1389+ }
1390+ } )
1391+
13201392 it . each ( [ 'clear' , 'blank_status' , 'blank_priority' ] as const ) (
13211393 'honors reviewed edits with a real-shaped managed grant: %s' ,
13221394 async ( scenario ) => {
0 commit comments