-
Notifications
You must be signed in to change notification settings - Fork 50
feat(agentex-ui): account picker via same-origin BFF proxy #350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
cee9dc9
feat(agentex-ui): account picker via same-origin BFF proxy
erichwoo-scale d97bc97
fix(agentex-ui): harden BFF proxy + move account picker to sidebar fo…
erichwoo-scale dee8932
fix(agentex-ui): show disabled empty account picker while loading
erichwoo-scale 0b756ec
refactor(agentex-ui): DRY account picker; reset open task on account …
erichwoo-scale 5f243a0
docs(agentex-ui): clarify applyBffCredentials owns credential headers
erichwoo-scale 6b8fb8f
fix(agentex-ui): reset account-scoped queries on switch to avoid stal…
erichwoo-scale 10022fe
docs(agentex-ui): tighten comments to the non-obvious
erichwoo-scale 54e5b75
fix(agentex-ui): reset agent selection to the home grid on account sw…
erichwoo-scale 7f6fe5d
fix(agentex-ui): strip the account-scoped _jwt cookie in the BFF
erichwoo-scale 9b500ec
Merge branch 'main' into feat/agentex-ui-account-picker
erichwoo-scale File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| /** Server-only platform API base for the BFF routes. Prefers SGP_API_URL, else the app URL. */ | ||
| export const SGP_BASE_URL = | ||
| process.env.SGP_API_URL ?? | ||
| (process.env.NEXT_PUBLIC_SGP_APP_URL | ||
| ? `${process.env.NEXT_PUBLIC_SGP_APP_URL}/api` | ||
| : undefined); | ||
|
|
||
| /** Return the Cookie header with the named cookie removed (null if nothing remains). */ | ||
| function stripCookie(header: string | null, name: string): string | null { | ||
| if (!header) return null; | ||
| const kept = header | ||
| .split(';') | ||
| .map(c => c.trim()) | ||
| .filter(c => { | ||
| const eq = c.indexOf('='); | ||
| return (eq === -1 ? c : c.slice(0, eq)) !== name; | ||
| }); | ||
| return kept.length ? kept.join('; ') : null; | ||
| } | ||
|
|
||
| /** | ||
| * Attach credentials to an upstream request's `headers` in place, so none reach client JS: | ||
| * forward `x-selected-account-id` (the upstream authorizes the account), drop any | ||
| * client-sent `authorization`, and forward cookies for the upstream's own auth — minus the | ||
| * account-scoped `_jwt` (access-profile) cookie. Cookie-auth backends prioritize `_jwt` over | ||
| * `x-selected-account-id`, so leaving it would pin the account to the one the user linked in | ||
| * with and ignore the selected account; identity still comes from `_identityJwt`. | ||
| */ | ||
| export async function applyBffCredentials( | ||
| req: Request, | ||
| headers: Headers | ||
| ): Promise<void> { | ||
| const accountId = req.headers.get('x-selected-account-id'); | ||
| if (accountId) headers.set('x-selected-account-id', accountId); | ||
| else headers.delete('x-selected-account-id'); | ||
|
|
||
| headers.delete('authorization'); | ||
|
|
||
| const cookie = stripCookie(req.headers.get('cookie'), '_jwt'); | ||
| if (cookie) headers.set('cookie', cookie); | ||
| else headers.delete('cookie'); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import { applyBffCredentials } from '@/app/api/_lib/bff'; | ||
|
|
||
| /** | ||
| * Same-origin BFF proxy for the Agentex API, so the upstream URL and credentials never | ||
| * reach client JS. applyBffCredentials attaches credentials server-side. | ||
| */ | ||
| export const dynamic = 'force-dynamic'; | ||
|
|
||
| const UPSTREAM = ( | ||
| process.env.AGENTEX_API_URL ?? 'http://localhost:5003' | ||
| ).replace(/\/$/, ''); | ||
|
|
||
| // Hop-by-hop headers to drop. Credential headers (cookie/authorization) are handled by | ||
| // applyBffCredentials, not here. | ||
| const STRIP_REQ = ['host', 'connection', 'content-length']; | ||
| const STRIP_RES = [ | ||
| 'content-encoding', | ||
| 'content-length', | ||
| 'transfer-encoding', | ||
| 'connection', | ||
| ]; | ||
|
|
||
| async function proxy( | ||
| req: Request, | ||
| ctx: { params: Promise<{ path?: string[] }> } | ||
| ): Promise<Response> { | ||
| const { path = [] } = await ctx.params; | ||
| const search = new URL(req.url).search; | ||
| const target = `${UPSTREAM}/${path.join('/')}${search}`; | ||
|
|
||
| const headers = new Headers(req.headers); | ||
| for (const h of STRIP_REQ) headers.delete(h); | ||
| await applyBffCredentials(req, headers); | ||
|
|
||
| const method = req.method.toUpperCase(); | ||
| const hasBody = method !== 'GET' && method !== 'HEAD'; | ||
| const upstream = await fetch(target, { | ||
| method, | ||
| headers, | ||
| body: hasBody ? req.body : undefined, | ||
| redirect: 'manual', | ||
| // @ts-expect-error `duplex` is required to stream a request body (undici) | ||
| duplex: 'half', | ||
| }); | ||
|
|
||
| // Pass the upstream body through unbuffered so SSE / streaming responses work. | ||
| const resHeaders = new Headers(upstream.headers); | ||
| for (const h of STRIP_RES) resHeaders.delete(h); | ||
| // Don't leak an upstream (internal) redirect target to the browser. | ||
| if (upstream.status >= 300 && upstream.status < 400) { | ||
| resHeaders.delete('location'); | ||
| } | ||
| return new Response(upstream.body, { | ||
| status: upstream.status, | ||
| headers: resHeaders, | ||
| }); | ||
| } | ||
|
|
||
| export { | ||
| proxy as DELETE, | ||
| proxy as GET, | ||
| proxy as HEAD, | ||
| proxy as OPTIONS, | ||
| proxy as PATCH, | ||
| proxy as POST, | ||
| proxy as PUT, | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { NextResponse } from 'next/server'; | ||
|
|
||
| import { applyBffCredentials, SGP_BASE_URL } from '@/app/api/_lib/bff'; | ||
|
|
||
| /** | ||
| * Scoped BFF proxy for the caller's accounts (access_profiles), used to bootstrap/switch | ||
| * the selected account. Only this path is exposed — not a catch-all — so the browser can't | ||
| * reach arbitrary platform endpoints with the server-attached credentials. | ||
| */ | ||
| export const dynamic = 'force-dynamic'; | ||
|
|
||
| export async function GET(request: Request): Promise<Response> { | ||
| if (!SGP_BASE_URL) { | ||
| return NextResponse.json( | ||
| { error: 'SGP is not configured. Set SGP_API_URL.' }, | ||
|
declan-scale marked this conversation as resolved.
|
||
| { status: 503 } | ||
| ); | ||
| } | ||
|
|
||
| const headers = new Headers({ accept: 'application/json' }); | ||
| await applyBffCredentials(request, headers); | ||
| const upstream = await fetch(`${SGP_BASE_URL}/user-info`, { headers }); | ||
| return new Response(upstream.body, { | ||
| status: upstream.status, | ||
| headers: { 'content-type': 'application/json' }, | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.