Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ const LOG_SSE_EVENTS = process.env.LOG_SSE_EVENTS === 'true';

const API_PROXY = process.env.API_PROXY;

// Cache for endpoint details to check task type
// Cache for endpoint details to check task type and OBO scopes
const endpointDetailsCache = new Map<
string,
{ task: string | undefined; timestamp: number }
{ task: string | undefined; userApiScopes: string[]; timestamp: number }
>();
const ENDPOINT_DETAILS_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes

Expand Down Expand Up @@ -261,7 +261,17 @@ const provider = createDatabricksProvider({
return provider;
}

// Get the task type of the serving endpoint
// Response type for serving endpoint details
interface EndpointDetailsResponse {
task: string | undefined;
auth_policy?: {
user_auth_policy: {
api_scopes: string[];
};
};
}

// Get the task type and OBO scopes of the serving endpoint
const getEndpointDetails = async (servingEndpoint: string) => {
const cached = endpointDetailsCache.get(servingEndpoint);
if (
Expand All @@ -284,15 +294,41 @@ const getEndpointDetails = async (servingEndpoint: string) => {
headers,
},
);
const data = (await response.json()) as { task: string | undefined };
const data = (await response.json()) as EndpointDetailsResponse;
const userApiScopes = data.auth_policy?.user_auth_policy?.api_scopes ?? [];

if (userApiScopes.length > 0) {
console.warn(
`⚠ OBO detected on endpoint "${servingEndpoint}". Required user authorization scopes: ${JSON.stringify(userApiScopes)}\n` +
` → Add these scopes to your app via the Databricks UI or in databricks.yml under resources.apps.<name>.user_authorization.scopes\n` +
` → See: https://docs.databricks.com/aws/en/dev-tools/databricks-apps/auth`,
);
}

const returnValue = {
task: data.task as string | undefined,
userApiScopes,
timestamp: Date.now(),
};
endpointDetailsCache.set(servingEndpoint, returnValue);
return returnValue;
};

/**
* Returns the OBO scopes for the configured serving endpoint, or empty array.
* Fetches endpoint details if not yet cached.
*/
export async function getEndpointOboScopes(): Promise<string[]> {
const servingEndpoint = process.env.DATABRICKS_SERVING_ENDPOINT;
if (!servingEndpoint) return [];
try {
const details = await getEndpointDetails(servingEndpoint);
return details.userApiScopes;
} catch {
return [];
}
}

// Create a smart provider wrapper that handles OAuth initialization
interface SmartProvider {
languageModel(id: string): Promise<LanguageModelV3>;
Expand Down
3 changes: 3 additions & 0 deletions e2e-chatbot-app-next/server/src/routes/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ chatRouter.post('/', requireAuth, async (req: Request, res: Response) => {
headers: {
[CONTEXT_HEADER_CONVERSATION_ID]: id,
[CONTEXT_HEADER_USER_ID]: session.user.email ?? session.user.id,
...(req.headers['x-forwarded-access-token']
? { 'x-forwarded-access-token': req.headers['x-forwarded-access-token'] as string }
: {}),
},
onFinish: ({ usage }) => {
finalUsage = usage;
Expand Down
10 changes: 8 additions & 2 deletions e2e-chatbot-app-next/server/src/routes/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@ import {
type Router as RouterType,
} from 'express';
import { isDatabaseAvailable } from '@chat-template/db';
import { getEndpointOboScopes } from '@chat-template/ai-sdk-providers';

export const configRouter: RouterType = Router();

/**
* GET /api/config - Get application configuration
* Returns feature flags based on environment configuration
* Returns feature flags and OBO status based on environment configuration
*/
configRouter.get('/', (_req: Request, res: Response) => {
configRouter.get('/', async (_req: Request, res: Response) => {
const oboScopes = await getEndpointOboScopes();
res.json({
features: {
chatHistory: isDatabaseAvailable(),
},
obo: {
enabled: oboScopes.length > 0,
requiredScopes: oboScopes,
},
});
});
6 changes: 6 additions & 0 deletions e2e-chatbot-app-next/tests/api-mocking/api-mock-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,16 @@ export const handlers = [

// Mock fetching endpoint details
// Returns agent/v1/responses to enable context injection testing
// Includes auth_policy to simulate an OBO-enabled endpoint
http.get(/\/api\/2\.0\/serving-endpoints\/[^/]+$/, () => {
return HttpResponse.json({
name: 'test-endpoint',
task: 'agent/v1/responses',
auth_policy: {
user_auth_policy: {
api_scopes: ['serving.serving-endpoints'],
},
},
});
}),

Expand Down