Skip to content

Commit d287f44

Browse files
FlyM1ssclaude
andauthored
feat(ext): send coarse-gate bearer header to backend calls (endpoint-auth P2) (#355)
* feat(ext): send coarse-gate bearer header from backend calls (F: endpoint-auth-01 P2) Phase 2 of the endpoint bearer-auth rollout: teach the Chrome extension to SEND `Authorization: Bearer <FINGPT_API_KEY>` on every backend call while its routes are still open — harmless now, required before Phase 3 flips the 14 extension views to enforced (a public extension can't be updated atomically, so the header must propagate to installs first). - webpack: add DefinePlugin baking process.env.FINGPT_API_KEY into the bundle (empty in local/dev builds; there was no build-injection machinery to reuse). - backendConfig.js: add getAuthHeaders() + build-time COARSE_GATE_KEY constant. A public bundle is extractable -> this is a coarse drive-by-abuse gate, NOT per-user auth (that is deferred to the api/identity.py login system). - Spread ...getAuthHeaders() into all 13 backend fetch header objects across api.js (10), config.js (1), components/link_manager.js (2). The xbrl <a download> is left untouched (exempt route, cannot carry a header). Verified: `FINGPT_API_KEY=devkey bun run build:full` bakes the literal once; a keyless build omits it (dev sends no header). Live DevTools on-the-wire check is the manual E2E handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MpjVcdhE4vq5CSEY7xmnpf * refactor(ext): route the 13 backend calls through a single authFetch() (simplify P2) /simplify pass on #355: the coarse-gate bearer header + credentials:'include' were hand-spliced into all 13 backend fetch sites in three stylistic variants. Centralize both into one authFetch(url, options) wrapper in backendConfig.js so the credentialed-session + Authorization pairing lives on one enforced path — a future 14th call site (or an edit to one of the 13) can no longer silently drop either. Behavior-preserving: every method/body/signal/Content-Type/Accept passes through untouched; getAuthHeaders() and the DefinePlugin key-bake are unchanged. Verified: keyless build bakes 0 keys, FINGPT_API_KEY=devkey build bakes exactly 1; backendConfig/sse tests 17/0; the 2 XSS/DOMPurify suite failures pre-exist (env). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MpjVcdhE4vq5CSEY7xmnpf --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent eec1ea8 commit d287f44

5 files changed

Lines changed: 50 additions & 27 deletions

File tree

Main/frontend/src/modules/api.js

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// api.js
22

3-
import { buildBackendUrl } from './backendConfig.js';
3+
import { buildBackendUrl, authFetch } from './backendConfig.js';
44
import { createSSEParser } from './sse.js';
55

66
// Session ID management
@@ -16,9 +16,8 @@ function setSessionId(sessionId) {
1616

1717
// Function to POST JSON to the server endpoint
1818
function postWebTextToServer(textContent, currentUrl) {
19-
return fetch(buildBackendUrl('/input_webtext/'), {
19+
return authFetch(buildBackendUrl('/input_webtext/'), {
2020
method: "POST",
21-
credentials: "include",
2221
headers: {
2322
"Content-Type": "application/json",
2423
},
@@ -101,9 +100,8 @@ function getChatResponse(question, selectedModel, promptMode, useRAG, useMCP) {
101100
// all modes share the two real endpoints now.
102101
const endpoint = promptMode ? 'get_adv_response' : 'get_chat_response';
103102

104-
return fetch(buildBackendUrl(`/${endpoint}/`), {
103+
return authFetch(buildBackendUrl(`/${endpoint}/`), {
105104
method: 'POST',
106-
credentials: 'include',
107105
headers: { 'Content-Type': 'application/json' },
108106
body: JSON.stringify(buildChatRequestBody(question, selectedModel, promptMode)),
109107
})
@@ -295,9 +293,8 @@ function getChatResponseStream(question, selectedModel, promptMode, useRAG, useM
295293
const parser = createSSEParser(handleServerEvent);
296294
const decoder = new TextDecoder('utf-8');
297295

298-
fetch(url, {
296+
authFetch(url, {
299297
method: 'POST',
300-
credentials: 'include',
301298
headers: {
302299
'Content-Type': 'application/json',
303300
'Accept': 'text/event-stream',
@@ -372,7 +369,7 @@ function getChatResponseStream(question, selectedModel, promptMode, useRAG, useM
372369

373370
// Function to clear messages
374371
function clearMessages() {
375-
return fetch(`${buildBackendUrl('/clear_messages/')}?use_memory=true&session_id=${currentSessionId}`, { method: "POST", credentials: "include" })
372+
return authFetch(`${buildBackendUrl('/clear_messages/')}?use_memory=true&session_id=${currentSessionId}`, { method: "POST" })
376373
.then(response => {
377374
if (!response.ok) {
378375
throw new Error('Network response was not ok');
@@ -402,7 +399,7 @@ function getSourceUrls(searchQuery, currentUrl) {
402399
const baseEndpoint = buildBackendUrl('/get_source_urls/');
403400
const requestUrl = queryString ? `${baseEndpoint}?${queryString}` : baseEndpoint;
404401

405-
return fetch(requestUrl, { method: "GET", credentials: "include" })
402+
return authFetch(requestUrl, { method: "GET" })
406403
.then(response => response.json())
407404
.then(data => {
408405
if (data.session_id) setSessionId(data.session_id);
@@ -419,9 +416,8 @@ function getSourceUrls(searchQuery, currentUrl) {
419416
function logQuestion(question, button) {
420417
const currentUrl = window.location.href;
421418

422-
return fetch(buildBackendUrl('/log_question/'), {
419+
return authFetch(buildBackendUrl('/log_question/'), {
423420
method: 'POST',
424-
credentials: 'include',
425421
headers: { 'Content-Type': 'application/json' },
426422
body: JSON.stringify({
427423
question: String(question),
@@ -447,9 +443,8 @@ function syncPreferredLinks() {
447443
try {
448444
const preferredLinks = JSON.parse(localStorage.getItem('preferredLinks') || '[]');
449445
if (preferredLinks.length > 0) {
450-
return fetch(buildBackendUrl('/api/sync_preferred_urls/'), {
446+
return authFetch(buildBackendUrl('/api/sync_preferred_urls/'), {
451447
method: 'POST',
452-
credentials: 'include',
453448
headers: {
454449
'Content-Type': 'application/json',
455450
},
@@ -497,9 +492,8 @@ function triggerAutoScrape(currentUrl) {
497492
return Promise.resolve({ status: 'skipped', reason: 'no_session_id' });
498493
}
499494

500-
return fetch(buildBackendUrl('/api/auto_scrape/'), {
495+
return authFetch(buildBackendUrl('/api/auto_scrape/'), {
501496
method: "POST",
502-
credentials: "include",
503497
headers: {
504498
"Content-Type": "application/json",
505499
},
@@ -528,10 +522,9 @@ function triggerAutoScrape(currentUrl) {
528522
// Layer 1 Validate: POST the current session to /api/axioms/validate/ and
529523
// return the per-claim verdicts for inline rendering.
530524
function validateClaims() {
531-
return fetch(buildBackendUrl('/api/axioms/validate/'), {
525+
return authFetch(buildBackendUrl('/api/axioms/validate/'), {
532526
method: 'POST',
533527
headers: { 'Content-Type': 'application/json' },
534-
credentials: 'include',
535528
body: JSON.stringify({ session_id: currentSessionId }),
536529
})
537530
.then((response) => {
@@ -546,9 +539,8 @@ function validateClaims() {
546539
// Used to decide whether to show the Validate button on a response bubble.
547540
function hasAxiomClaims() {
548541
const params = currentSessionId ? `?session_id=${encodeURIComponent(currentSessionId)}` : '';
549-
return fetch(buildBackendUrl(`/api/axioms/has_claims/${params}`), {
542+
return authFetch(buildBackendUrl(`/api/axioms/has_claims/${params}`), {
550543
method: 'GET',
551-
credentials: 'include',
552544
})
553545
.then((response) => (response.ok ? response.json() : { has_claims: false }))
554546
.catch(() => ({ has_claims: false }));

Main/frontend/src/modules/backendConfig.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,34 @@
44
const DEFAULT_BACKEND_BASE_URL = 'https://agenticfinsearch.org';
55
let cachedBaseUrl = null;
66

7+
// Coarse-gate bearer key, baked at build time by the webpack DefinePlugin from
8+
// the build environment (process.env.FINGPT_API_KEY). Empty in local/dev builds,
9+
// so no Authorization header is sent and the extension works against an open dev
10+
// backend. A public extension bundle is EXTRACTABLE — this is a COARSE gate
11+
// against drive-by API abuse, NOT per-user auth (deferred to the backend login
12+
// system, api/identity.py). Tests/dev may override via window.FINGPT_API_KEY.
13+
const COARSE_GATE_KEY =
14+
(typeof window !== 'undefined' && window.FINGPT_API_KEY) ||
15+
process.env.FINGPT_API_KEY ||
16+
'';
17+
18+
function getAuthHeaders() {
19+
return COARSE_GATE_KEY ? { Authorization: `Bearer ${COARSE_GATE_KEY}` } : {};
20+
}
21+
22+
// Single enforced path for backend calls: every request to our own backend must
23+
// carry the credentialed session AND the coarse-gate bearer header, so both are
24+
// attached here rather than hand-spliced at each call site (where a future one
25+
// could silently drop either). Caller-supplied method/body/signal/headers pass
26+
// through untouched; the Authorization header is merged in last.
27+
function authFetch(url, options = {}) {
28+
return fetch(url, {
29+
...options,
30+
credentials: 'include',
31+
headers: { ...options.headers, ...getAuthHeaders() },
32+
});
33+
}
34+
735
// Hosts the extension is permitted to talk to. Overrides come from
836
// window.AGENTIC_BACKEND_URL or localStorage['agenticBackendUrl']; because
937
// requests are sent with credentials:'include', an unconstrained override
@@ -96,4 +124,4 @@ function buildBackendUrl(path = '/') {
96124
return `${baseUrl}${sanitizedPath}`;
97125
}
98126

99-
export { getBackendBaseUrl, buildBackendUrl, normalizeBaseUrl };
127+
export { getBackendBaseUrl, buildBackendUrl, normalizeBaseUrl, getAuthHeaders, authFetch };

Main/frontend/src/modules/components/link_manager.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { buildBackendUrl } from '../backendConfig.js';
1+
import { buildBackendUrl, authFetch } from '../backendConfig.js';
22

33
export function createLinkManager() {
44
// Container for the whole link manager
@@ -47,9 +47,8 @@ export function createLinkManager() {
4747
function syncWithBackend(links) {
4848
// Optional: Sync with backend if available
4949
if (typeof fetch !== 'undefined') {
50-
fetch(buildBackendUrl('/api/sync_preferred_urls/'), {
50+
authFetch(buildBackendUrl('/api/sync_preferred_urls/'), {
5151
method: 'POST',
52-
credentials: 'include',
5352
headers: {
5453
'Content-Type': 'application/json',
5554
},
@@ -176,9 +175,8 @@ export function createLinkManager() {
176175
return;
177176
}
178177

179-
fetch(buildBackendUrl('/api/get_preferred_urls/'), {
178+
authFetch(buildBackendUrl('/api/get_preferred_urls/'), {
180179
method: 'GET',
181-
credentials: 'include'
182180
})
183181
.then(response => {
184182
if (!response.ok) {

Main/frontend/src/modules/config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// config.js
22

3-
import { buildBackendUrl } from './backendConfig.js';
3+
import { buildBackendUrl, authFetch } from './backendConfig.js';
44

55
// Available models - will be populated from backend
66
let availableModels = [];
@@ -11,7 +11,7 @@ let selectedModel = "FinGPT";
1111

1212
// Fetch available models from backend
1313
async function fetchAvailableModels() {
14-
const response = await fetch(buildBackendUrl('/api/get_available_models/'), { credentials: 'include' });
14+
const response = await authFetch(buildBackendUrl('/api/get_available_models/'), { method: 'GET' });
1515
if (!response.ok) {
1616
throw new Error(`Failed to fetch models from backend. HTTP error! status: ${response.status}`);
1717
}

Main/frontend/webpack.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ module.exports = {
115115
hints: "warning"
116116
},
117117
plugins: [
118+
// Bake the coarse-gate API key from the build env into the bundle. Empty when
119+
// FINGPT_API_KEY is unset (local/dev builds) -> getAuthHeaders() returns {}.
120+
new webpack.DefinePlugin({
121+
'process.env.FINGPT_API_KEY': JSON.stringify(process.env.FINGPT_API_KEY || ''),
122+
}),
118123
new webpack.BannerPlugin({
119124
banner: '// @charset "UTF-8";',
120125
raw: true

0 commit comments

Comments
 (0)