Skip to content

Commit eb80955

Browse files
Merge branch 'oracle-devrel:main' into main
2 parents 41beb1f + 90d157c commit eb80955

41 files changed

Lines changed: 1851 additions & 187 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
1-
import { dirname } from "path";
2-
import { fileURLToPath } from "url";
3-
import { FlatCompat } from "@eslint/eslintrc";
1+
// eslint-config-next v16 ships a native flat config — the old FlatCompat bridge
2+
// crashes on it (circular structure error). `next lint` itself was removed in
3+
// Next 16, so `npm run lint` invokes eslint directly against this config.
4+
import coreWebVitals from "eslint-config-next/core-web-vitals";
45

5-
const __filename = fileURLToPath(import.meta.url);
6-
const __dirname = dirname(__filename);
7-
8-
const compat = new FlatCompat({
9-
baseDirectory: __dirname,
10-
});
11-
12-
const eslintConfig = [...compat.extends("next/core-web-vitals")];
6+
const eslintConfig = [
7+
{ ignores: [".next/**", "node_modules/**", "public/**", "tests/manual/**", ".claude/**"] },
8+
...coreWebVitals,
9+
{
10+
rules: {
11+
// React Compiler advisories introduced with the v16 ruleset. The codebase
12+
// predates them (localStorage hydration via setState-in-effect everywhere);
13+
// they flag working patterns, not defects. Kept visible as warnings so new
14+
// code can avoid them — `rules-of-hooks` stays an error.
15+
"react-hooks/set-state-in-effect": "warn",
16+
"react-hooks/static-components": "warn",
17+
"react-hooks/purity": "warn",
18+
"react-hooks/preserve-manual-memoization": "warn",
19+
"react-hooks/use-memo": "warn",
20+
"react-hooks/refs": "warn",
21+
// Cosmetic: unescaped apostrophes/quotes in JSX copy.
22+
"react/no-unescaped-entities": "warn",
23+
},
24+
},
25+
];
1326

1427
export default eslintConfig;

ai/gen-ai-agents/oci-enterprise-ai-chat/files/package-lock.json

Lines changed: 42 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ai/gen-ai-agents/oci-enterprise-ai-chat/files/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"name": "oci-enterprise-ai-chat",
3-
"version": "0.9.9",
3+
"version": "0.10.0",
44
"private": true,
55
"scripts": {
66
"dev": "next dev --turbopack",
77
"build": "next build",
88
"start": "next start",
9-
"lint": "next lint",
9+
"lint": "eslint .",
1010
"test": "playwright test"
1111
},
1212
"dependencies": {
@@ -39,8 +39,10 @@
3939
"devDependencies": {
4040
"@eslint/eslintrc": "^3",
4141
"@iconify/react": "^6.0.2",
42+
"@playwright/test": "^1.60.0",
4243
"eslint": "^9",
4344
"eslint-config-next": "16.1.3",
44-
"playwright": "^1.57.0"
45+
"playwright": "^1.57.0",
46+
"undici": "^8.4.1"
4547
}
4648
}

ai/gen-ai-agents/oci-enterprise-ai-chat/files/playwright.config.js

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import { defineConfig, devices } from '@playwright/test';
2+
import os from 'node:os';
3+
import path from 'node:path';
24

35
/**
46
* @see https://playwright.dev/docs/test-configuration
57
*/
68
export default defineConfig({
79
testDir: './tests',
10+
/* OUTSIDE the project tree: writing artifacts under ./test-results while the
11+
dev server watches the repo triggers turbopack Fast Refresh mid-test, which
12+
remounts React and wipes in-flight chat state — flaking every UI spec that
13+
runs after the first failure writes its artifacts. */
14+
outputDir: path.join(os.tmpdir(), 'oci-enterprise-ai-chat-test-results'),
815
/* Run tests in files in parallel */
916
fullyParallel: true,
1017
/* Fail the build on CI if you accidentally left test.only in the source code. */
@@ -13,8 +20,9 @@ export default defineConfig({
1320
retries: process.env.CI ? 2 : 0,
1421
/* Opt out of parallel tests on CI. */
1522
workers: process.env.CI ? 1 : undefined,
16-
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
17-
reporter: 'html',
23+
/* `list` keeps `npm test` non-interactive locally (the `html` reporter spawns
24+
a blocking server on failures); CI keeps the rich html report. */
25+
reporter: process.env.CI ? 'html' : 'list',
1826
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
1927
use: {
2028
/* Base URL to use in actions like `await page.goto('/')`. */
@@ -24,23 +32,14 @@ export default defineConfig({
2432
trace: 'on-first-retry',
2533
},
2634

27-
/* Configure projects for major browsers */
35+
/* Chromium only: it's the only browser installed locally and the suite is
36+
unit/API-heavy — cross-browser adds runtime without much signal here. */
2837
projects: [
2938
{
3039
name: 'chromium',
3140
use: { ...devices['Desktop Chrome'] },
3241
},
3342

34-
{
35-
name: 'firefox',
36-
use: { ...devices['Desktop Firefox'] },
37-
},
38-
39-
{
40-
name: 'webkit',
41-
use: { ...devices['Desktop Safari'] },
42-
},
43-
4443
/* Test against mobile viewports. */
4544
// {
4645
// name: 'Mobile Chrome',

ai/gen-ai-agents/oci-enterprise-ai-chat/files/src/app/api/mcp/oauth/callback/route.js

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,28 @@ export async function GET(request) {
6969
log.info('Token exchange successful', { endpoint: pending.endpoint });
7070

7171
// 3. Persist tokens in a signed cookie
72+
//
73+
// Two storage shapes depending on what the provider issued:
74+
// - refresh_token present (IDCS, Google with offline access): store ONLY the
75+
// refresh token. The access JWT (~3KB for IDCS) would push the signed cookie
76+
// past the browser's ~4KB limit and the cookie gets dropped (hasToken=false).
77+
// /api/mcp/oauth/token mints a fresh access token on demand.
78+
// - NO refresh_token (GitHub OAuth apps, Slack without rotation): store the
79+
// access token itself — those are small opaque strings, and without it the
80+
// cookie would be unusable (nothing to mint from). When the provider also
81+
// omits expires_in, the token is long-lived; cap it at the cookie's 30 days.
82+
const hasRefresh = !!tokenData.refresh_token;
83+
const expiresInMs = tokenData.expires_in
84+
? tokenData.expires_in * 1000
85+
: (hasRefresh ? 3600 * 1000 : 30 * 24 * 60 * 60 * 1000);
7286
const tokens = await signPayload({
7387
endpoint: pending.endpoint,
7488
clientId: pending.clientId,
7589
clientSecret: pending.clientSecret,
7690
tokenEndpoint: pending.tokenEndpoint,
77-
// Access JWT (~3KB for IDCS) is NOT stored — it would push the signed cookie
78-
// past the browser's ~4KB limit and the cookie gets dropped (hasToken=false).
79-
// Store only the small refresh token; /api/mcp/oauth/token mints a fresh
80-
// access token on demand. This also fixes access-token expiry.
81-
refreshToken: tokenData.refresh_token,
82-
expiresAt: Date.now() + (tokenData.expires_in || 3600) * 1000,
91+
refreshToken: tokenData.refresh_token || null,
92+
...(hasRefresh ? {} : { accessToken: tokenData.access_token }),
93+
expiresAt: Date.now() + expiresInMs,
8394
});
8495

8596
const response = NextResponse.redirect(returnUrl(request, 'success', pending));
Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { NextResponse } from 'next/server';
2-
import { verifyPayload, signPayload, refreshAccessToken, tokenCookieName } from '../../../../lib/mcp-oauth';
2+
import { verifyPayload, signPayload, mintAccessToken, tokenCookieName } from '../../../../lib/mcp-oauth';
3+
import { createLogger } from '../../../../lib/logger';
34

45
/**
56
* GET /api/mcp/oauth/token?endpoint=...
67
* Returns the current access token for a given MCP endpoint.
78
* Used by the frontend to pass the token to OCI Responses API.
89
*/
910
export async function GET(request) {
11+
const log = createLogger('mcp-oauth-token');
1012
const endpoint = new URL(request.url).searchParams.get('endpoint');
1113
if (!endpoint) {
1214
return NextResponse.json({ error: 'endpoint is required' }, { status: 400 });
@@ -16,27 +18,50 @@ export async function GET(request) {
1618
const tokenCookie = request.cookies.get(cookieName)?.value;
1719
const tokens = await verifyPayload(tokenCookie);
1820

19-
if (!tokens || !tokens.refreshToken) {
21+
if (!tokens || (!tokens.refreshToken && !tokens.accessToken)) {
22+
log.info('No usable token', { endpoint: endpoint.slice(0, 60), hasCookie: !!tokenCookie, verified: !!tokens });
2023
return NextResponse.json({ hasToken: false });
2124
}
2225

23-
// Lightweight presence check (used by Settings to show authorized/needs_auth).
24-
// Does NOT consume the refresh token.
26+
// Access-token-only cookie: the provider issued no refresh token (GitHub OAuth
27+
// apps, Slack without rotation). Serve the stored token while it's valid;
28+
// once expired there is nothing to mint from — the user must re-authorize.
29+
if (!tokens.refreshToken) {
30+
const valid = tokens.expiresAt > Date.now() + 30000;
31+
if (new URL(request.url).searchParams.get('probe') === 'true') {
32+
return NextResponse.json({ hasToken: valid });
33+
}
34+
return valid
35+
? NextResponse.json({ hasToken: true, accessToken: tokens.accessToken })
36+
: NextResponse.json({ hasToken: false });
37+
}
38+
39+
// Settings status probe. It used to only check that the cookie EXISTS, which
40+
// showed "Authorized" while the refresh token was already dead — the user then
41+
// hit the auth banner on the very next message. Verify mintability for real;
42+
// the shared mint cache makes this one IdP roundtrip per token lifetime.
2543
if (new URL(request.url).searchParams.get('probe') === 'true') {
26-
return NextResponse.json({ hasToken: true });
44+
try {
45+
await mintAccessToken(tokens.tokenEndpoint, tokens.refreshToken, tokens.clientId, tokens.clientSecret);
46+
return NextResponse.json({ hasToken: true });
47+
} catch (err) {
48+
log.error('Probe mint failed', { endpoint: endpoint.slice(0, 60), error: (err?.message || '').slice(0, 200) });
49+
return NextResponse.json({ hasToken: false });
50+
}
2751
}
2852

2953
// The cookie stores ONLY the refresh token (the IDCS access JWT is ~3KB and would
3054
// blow past the browser's ~4KB cookie limit). Always mint a fresh access token
3155
// from the refresh token, and persist the (possibly rotated) refresh token.
3256
try {
33-
const refreshed = await refreshAccessToken(
57+
const minted = await mintAccessToken(
3458
tokens.tokenEndpoint, tokens.refreshToken, tokens.clientId, tokens.clientSecret
3559
);
36-
tokens.refreshToken = refreshed.refresh_token || tokens.refreshToken;
37-
tokens.expiresAt = Date.now() + (refreshed.expires_in || 3600) * 1000;
60+
tokens.refreshToken = minted.refreshToken;
61+
tokens.expiresAt = minted.expiresAt;
62+
log.info('Access token served', { endpoint: endpoint.slice(0, 60) });
3863

39-
const response = NextResponse.json({ hasToken: true, accessToken: refreshed.access_token });
64+
const response = NextResponse.json({ hasToken: true, accessToken: minted.accessToken });
4065
const isHttps = (request.headers.get('x-forwarded-proto') || 'http') === 'https';
4166
response.cookies.set(cookieName, await signPayload(tokens), {
4267
httpOnly: true,
@@ -46,7 +71,10 @@ export async function GET(request) {
4671
path: '/',
4772
});
4873
return response;
49-
} catch {
74+
} catch (err) {
75+
// Surface the IdP's actual error — a silent hasToken:false here made the
76+
// Nl2Sql (Text-to-SQL) tool quietly drop out of requests with no trace of why.
77+
log.error('Refresh failed', { endpoint: endpoint.slice(0, 60), error: (err?.message || '').slice(0, 300) });
5078
return NextResponse.json({ hasToken: false });
5179
}
5280
}

0 commit comments

Comments
 (0)