fix(C6): require explicit FMP API key
Remove the silent shared demo-key fallback for FMP-backed research and screener routes, document the required key, and make backend/.env.example trackable so setup guidance has one source of truth. Refs: docs/AUDIT_REDESIGN.md item C6. Co-Authored-By: GPT-5 Codex <noreply@openai.com>
This commit is contained in:
parent
5ce3cc92f0
commit
1377bf2453
@ -126,6 +126,7 @@ Each surface has its own `.env.example`. The root `.env.example` is the comprehe
|
|||||||
| `PLATFORM_JWT_PUBLIC_KEY` or `PLATFORM_JWT_JWKS_URL` | Prod | Platform JWT verification key |
|
| `PLATFORM_JWT_PUBLIC_KEY` or `PLATFORM_JWT_JWKS_URL` | Prod | Platform JWT verification key |
|
||||||
| `JWT_SECRET` | Dev | Legacy/local JWT secret when `PLATFORM_AUTH_ENABLED=false` |
|
| `JWT_SECRET` | Dev | Legacy/local JWT secret when `PLATFORM_AUTH_ENABLED=false` |
|
||||||
| `ALPACA_API_KEY` / `ALPACA_API_SECRET` | For trading | Exchange credentials |
|
| `ALPACA_API_KEY` / `ALPACA_API_SECRET` | For trading | Exchange credentials |
|
||||||
|
| `FMP_API_KEY` | For research/screener | Financial Modeling Prep key required by `/api/research/*` and `/api/screener`; the shared `demo` key is intentionally rejected by the backend |
|
||||||
| `OPENAI_API_KEY` | For AI rules | Primary LLM provider |
|
| `OPENAI_API_KEY` | For AI rules | Primary LLM provider |
|
||||||
| `ENABLE_TRADING` | — | Set `true` to enable live order execution (default `false`) |
|
| `ENABLE_TRADING` | — | Set `true` to enable live order execution (default `false`) |
|
||||||
| `PAPER_TRADING` | — | Set `true` for paper mode |
|
| `PAPER_TRADING` | — | Set `true` for paper mode |
|
||||||
|
|||||||
128
backend/.env.example
Normal file
128
backend/.env.example
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# ByteLyst Trading — Backend Environment Configuration
|
||||||
|
# Copy this file to .env and fill in your values.
|
||||||
|
# Production: pull secrets from Azure Key Vault (see docs/AZURE_INFRASTRUCTURE.md)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Server ---
|
||||||
|
API_PORT=4018
|
||||||
|
CORS_ALLOWED_ORIGINS=http://localhost:5173
|
||||||
|
|
||||||
|
# --- Platform Auth (required for production) ---
|
||||||
|
# Set PLATFORM_AUTH_ENABLED=true to require platform JWT (RS256) verification.
|
||||||
|
# For local dev with legacy Supabase JWTs, set false and provide JWT_SECRET.
|
||||||
|
PLATFORM_AUTH_ENABLED=false
|
||||||
|
PLATFORM_API_URL=http://localhost:4003/api
|
||||||
|
PLATFORM_JWT_ISSUER=bytelyst-platform
|
||||||
|
PLATFORM_JWT_PUBLIC_KEY=
|
||||||
|
PLATFORM_JWT_JWKS_URL=
|
||||||
|
JWT_SECRET=change-me-for-local-dev
|
||||||
|
|
||||||
|
# --- Azure Key Vault (optional — skipped if unset) ---
|
||||||
|
# Set to trigger automatic secret resolution at startup via DefaultAzureCredential.
|
||||||
|
# Pulls invttrdg-* secrets into the env vars below before the app starts.
|
||||||
|
AZURE_KEYVAULT_URL=
|
||||||
|
|
||||||
|
# --- Cosmos DB (primary data store) ---
|
||||||
|
# Pulled from Key Vault in production (invttrdg-cosmos-*).
|
||||||
|
COSMOS_ENDPOINT=https://cosmos-mywisprai.documents.azure.com:443/
|
||||||
|
COSMOS_KEY=your_cosmos_key
|
||||||
|
COSMOS_DATABASE=invttrdg
|
||||||
|
|
||||||
|
# --- Azure OpenAI (AI Foundry — preferred over direct OpenAI in production) ---
|
||||||
|
# Pulled from Key Vault in production (invttrdg-azure-openai-*).
|
||||||
|
# When set, LLM_PROVIDER is auto-detected as 'azure'.
|
||||||
|
AZURE_OPENAI_ENDPOINT=
|
||||||
|
AZURE_OPENAI_KEY=
|
||||||
|
AZURE_OPENAI_DEPLOYMENT=gpt-4o
|
||||||
|
|
||||||
|
# --- Plug-and-Play Configuration ---
|
||||||
|
PROVIDER=alpaca # Options: 'alpaca' or 'ccxt'
|
||||||
|
|
||||||
|
# Asset Details
|
||||||
|
SYMBOL=BTC/USD
|
||||||
|
TIMEFRAME=1Min
|
||||||
|
POLLING_INTERVAL=60000
|
||||||
|
|
||||||
|
# --- Alpaca Settings ---
|
||||||
|
ALPACA_API_KEY=your_key
|
||||||
|
ALPACA_API_SECRET=your_secret
|
||||||
|
PAPER_TRADING=true
|
||||||
|
ASSET_CLASS=crypto # 'crypto' or 'us_equity'
|
||||||
|
|
||||||
|
# --- Research Data (Financial Modeling Prep) ---
|
||||||
|
# Required for /api/research/* and /api/screener. Free tier: 250 req/day.
|
||||||
|
# Register at https://financialmodelingprep.com/developer/docs; do not use the
|
||||||
|
# shared "demo" key outside ad-hoc manual experiments.
|
||||||
|
FMP_API_KEY=your_fmp_key
|
||||||
|
|
||||||
|
# --- CCXT Settings ---
|
||||||
|
EXCHANGE=binance
|
||||||
|
CCXT_API_KEY=your_key
|
||||||
|
CCXT_API_SECRET=your_secret
|
||||||
|
|
||||||
|
# --- Notifications ---
|
||||||
|
WEBHOOK_URL=https://discord.com/api/webhooks/your/url
|
||||||
|
|
||||||
|
# --- AI Configuration ---
|
||||||
|
AI_PROVIDER=perplexity
|
||||||
|
PERPLEXITY_API_KEY=your_perplexity_key
|
||||||
|
OPENAI_API_KEY=your_openai_key
|
||||||
|
GEMINI_API_KEY=your_gemini_key
|
||||||
|
AI_FALLBACK_LIST=perplexity,openai,gemini
|
||||||
|
AI_MODEL=sonar
|
||||||
|
AI_CONFIDENCE_THRESHOLD=70
|
||||||
|
AI_CACHE_HOURS=4
|
||||||
|
|
||||||
|
# --- Features ---
|
||||||
|
LOW_STRESS_MODE=false
|
||||||
|
ENABLE_TREND_ALERTS=true
|
||||||
|
ENABLE_PULSE_ALERTS=true
|
||||||
|
ENABLE_TRADING=true
|
||||||
|
TOTAL_CAPITAL=1000
|
||||||
|
MAX_OPEN_TRADES=3
|
||||||
|
|
||||||
|
# Feature flags (opt-out: omit or true = enabled, false = disabled)
|
||||||
|
ENABLE_BACKTEST=false
|
||||||
|
BACKTEST_CUSTOMER_ENABLED=false
|
||||||
|
TAB_MARKETPLACE_ENABLED=true
|
||||||
|
TAB_MEMBERSHIP_ENABLED=true
|
||||||
|
|
||||||
|
ENABLE_STRICT_CAPITAL_GUARD=true
|
||||||
|
STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT=1.0
|
||||||
|
STRICT_CAPITAL_FEE_BUFFER_PCT=0.15
|
||||||
|
STRICT_CAPITAL_MIN_RESERVE_USD=0
|
||||||
|
|
||||||
|
# --- Alpaca Omnibus Sub-tagging ---
|
||||||
|
ENABLE_ALPACA_SUBTAG=false
|
||||||
|
SUBTAG_OMNIBUS_ONLY=true
|
||||||
|
ALPACA_SUBTAG_ENV=paper
|
||||||
|
ALPACA_SUBTAG_MAX_LENGTH=48
|
||||||
|
ALPACA_SUBTAG_DISABLE_FOR_EXCHANGE=
|
||||||
|
ALPACA_OMNIBUS_PROFILE_ALLOWLIST=
|
||||||
|
|
||||||
|
# --- Reconciliation EXIT Backfill Safety ---
|
||||||
|
ENABLE_RECON_EXIT_BACKFILL=true
|
||||||
|
RECON_EXIT_BACKFILL_DRY_RUN=true
|
||||||
|
RECON_EXIT_BACKFILL_REQUIRE_PAUSE=true
|
||||||
|
RECON_EXIT_BACKFILL_DUST_ABS_QTY=0.001
|
||||||
|
RECON_EXIT_BACKFILL_DUST_REL_PCT=0.002
|
||||||
|
RECON_EXIT_BACKFILL_LOOKBACK_HOURS=72
|
||||||
|
RECON_EXIT_BACKFILL_REQUIRE_STRONG_ATTRIBUTION=true
|
||||||
|
RECON_EXIT_BACKFILL_ALLOW_HEURISTIC_MATCH=false
|
||||||
|
RECON_EXIT_BACKFILL_FILL_AFTER_TRADE_GRACE_MINUTES=5
|
||||||
|
|
||||||
|
# --- Reconciliation Parity Watchdog Auto-Resume ---
|
||||||
|
ENABLE_RECON_WATCHDOG_AUTO_RESUME=true
|
||||||
|
RECON_WATCHDOG_AUTO_RESUME_MIN_PAUSE_MS=900000
|
||||||
|
RECON_WATCHDOG_AUTO_RESUME_CLEAN_CYCLES=2
|
||||||
|
RECON_WATCHDOG_AUTO_RESUME_COOLDOWN_MS=1800000
|
||||||
|
|
||||||
|
# --- Supabase (legacy fallback — Cosmos DB is primary) ---
|
||||||
|
# Required only if Cosmos DB is not configured or for legacy auth fallback.
|
||||||
|
SUPABASE_URL=your_supabase_url
|
||||||
|
SUPABASE_KEY=your_supabase_key
|
||||||
|
|
||||||
|
# --- Product Identity ---
|
||||||
|
# Auto-set from Key Vault (invttrdg-product-id) in production; default is fine for local dev.
|
||||||
|
PRODUCT_ID=invttrdg
|
||||||
2
backend/.gitignore
vendored
2
backend/.gitignore
vendored
@ -1,8 +1,8 @@
|
|||||||
node_modules
|
node_modules
|
||||||
! .env.example
|
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
*.env
|
*.env
|
||||||
|
!.env.example
|
||||||
old_env.txt
|
old_env.txt
|
||||||
new_env.txt
|
new_env.txt
|
||||||
temp_env.txt
|
temp_env.txt
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export const config = {
|
|||||||
|
|
||||||
// Research data — Financial Modeling Prep (free tier: 250 req/day)
|
// Research data — Financial Modeling Prep (free tier: 250 req/day)
|
||||||
// Register free at https://financialmodelingprep.com/developer/docs
|
// Register free at https://financialmodelingprep.com/developer/docs
|
||||||
FMP_API_KEY: process.env.FMP_API_KEY || 'demo',
|
FMP_API_KEY: process.env.FMP_API_KEY || '',
|
||||||
|
|
||||||
// Supabase
|
// Supabase
|
||||||
SUPABASE_URL: process.env.SUPABASE_URL || '',
|
SUPABASE_URL: process.env.SUPABASE_URL || '',
|
||||||
|
|||||||
@ -130,6 +130,15 @@ const normalizeNewsSymbolsQuery = (value: unknown): string => {
|
|||||||
return symbols.join(',');
|
return symbols.join(',');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getConfiguredFmpApiKey = (): string => {
|
||||||
|
const apiKey = config.FMP_API_KEY.trim();
|
||||||
|
if (!apiKey || apiKey.toLowerCase() === 'demo') {
|
||||||
|
throw new Error('FMP_API_KEY is required for research and screener endpoints');
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiKey;
|
||||||
|
};
|
||||||
|
|
||||||
interface TradeAuditEvent {
|
interface TradeAuditEvent {
|
||||||
event: string;
|
event: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
@ -2805,7 +2814,7 @@ RULES:
|
|||||||
try {
|
try {
|
||||||
const symbol = String(req.query.symbol || '').trim().toUpperCase();
|
const symbol = String(req.query.symbol || '').trim().toUpperCase();
|
||||||
if (!symbol) return res.status(400).json({ error: 'symbol required' });
|
if (!symbol) return res.status(400).json({ error: 'symbol required' });
|
||||||
const apiKey = process.env.FMP_API_KEY || 'demo';
|
const apiKey = getConfiguredFmpApiKey();
|
||||||
const url = `https://financialmodelingprep.com/api/v3/profile/${symbol}?apikey=${apiKey}`;
|
const url = `https://financialmodelingprep.com/api/v3/profile/${symbol}?apikey=${apiKey}`;
|
||||||
const data = await fetchFmpJson(url) as any;
|
const data = await fetchFmpJson(url) as any;
|
||||||
res.json(Array.isArray(data) ? data[0] ?? {} : data);
|
res.json(Array.isArray(data) ? data[0] ?? {} : data);
|
||||||
@ -2822,7 +2831,7 @@ RULES:
|
|||||||
try {
|
try {
|
||||||
const symbol = String(req.query.symbol || '').trim().toUpperCase();
|
const symbol = String(req.query.symbol || '').trim().toUpperCase();
|
||||||
if (!symbol) return res.status(400).json({ error: 'symbol required' });
|
if (!symbol) return res.status(400).json({ error: 'symbol required' });
|
||||||
const apiKey = process.env.FMP_API_KEY || 'demo';
|
const apiKey = getConfiguredFmpApiKey();
|
||||||
const url = `https://financialmodelingprep.com/api/v3/key-metrics/${symbol}?limit=4&apikey=${apiKey}`;
|
const url = `https://financialmodelingprep.com/api/v3/key-metrics/${symbol}?limit=4&apikey=${apiKey}`;
|
||||||
const data = await fetchFmpJson(url) as any;
|
const data = await fetchFmpJson(url) as any;
|
||||||
res.json(Array.isArray(data) ? data[0] ?? {} : data);
|
res.json(Array.isArray(data) ? data[0] ?? {} : data);
|
||||||
@ -2839,7 +2848,7 @@ RULES:
|
|||||||
try {
|
try {
|
||||||
const symbol = String(req.query.symbol || '').trim().toUpperCase();
|
const symbol = String(req.query.symbol || '').trim().toUpperCase();
|
||||||
if (!symbol) return res.status(400).json({ error: 'symbol required' });
|
if (!symbol) return res.status(400).json({ error: 'symbol required' });
|
||||||
const apiKey = process.env.FMP_API_KEY || 'demo';
|
const apiKey = getConfiguredFmpApiKey();
|
||||||
const url = `https://financialmodelingprep.com/api/v3/historical/earning_calendar/${symbol}?limit=8&apikey=${apiKey}`;
|
const url = `https://financialmodelingprep.com/api/v3/historical/earning_calendar/${symbol}?limit=8&apikey=${apiKey}`;
|
||||||
const data = await fetchFmpJson(url) as any;
|
const data = await fetchFmpJson(url) as any;
|
||||||
res.json({ earnings: Array.isArray(data) ? data : [] });
|
res.json({ earnings: Array.isArray(data) ? data : [] });
|
||||||
@ -2854,7 +2863,7 @@ RULES:
|
|||||||
// ── Screener: stock screener from FMP ────────────────────────────────
|
// ── Screener: stock screener from FMP ────────────────────────────────
|
||||||
this.app.get('/api/screener', this.requireAuth, async (req, res) => {
|
this.app.get('/api/screener', this.requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const apiKey = process.env.FMP_API_KEY || 'demo';
|
const apiKey = getConfiguredFmpApiKey();
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
const sector = String(req.query.sector || '').trim();
|
const sector = String(req.query.sector || '').trim();
|
||||||
if (sector && sector !== 'All') {
|
if (sector && sector !== 'All') {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url';
|
|||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const apiServerSource = readFileSync(join(__dirname, 'src/services/apiServer.ts'), 'utf8');
|
const apiServerSource = readFileSync(join(__dirname, 'src/services/apiServer.ts'), 'utf8');
|
||||||
|
const configSource = readFileSync(join(__dirname, 'src/config/index.ts'), 'utf8');
|
||||||
|
|
||||||
function escapeRegExp(value: string) {
|
function escapeRegExp(value: string) {
|
||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
@ -33,6 +34,10 @@ function assertSourceIncludes(fragment: string, message: string) {
|
|||||||
assert.ok(apiServerSource.includes(fragment), message);
|
assert.ok(apiServerSource.includes(fragment), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertConfigIncludes(fragment: string, message: string) {
|
||||||
|
assert.ok(configSource.includes(fragment), message);
|
||||||
|
}
|
||||||
|
|
||||||
function assertSourceMatches(pattern: RegExp, message: string) {
|
function assertSourceMatches(pattern: RegExp, message: string) {
|
||||||
assert.match(apiServerSource, pattern, message);
|
assert.match(apiServerSource, pattern, message);
|
||||||
}
|
}
|
||||||
@ -151,6 +156,18 @@ function testFmpProxyContracts() {
|
|||||||
"import { fetchFmpJson, FmpFetchError } from './fmpCache.js';",
|
"import { fetchFmpJson, FmpFetchError } from './fmpCache.js';",
|
||||||
'FMP routes must use the shared cache helper and typed upstream error',
|
'FMP routes must use the shared cache helper and typed upstream error',
|
||||||
);
|
);
|
||||||
|
assertConfigIncludes(
|
||||||
|
"FMP_API_KEY: process.env.FMP_API_KEY || '',",
|
||||||
|
'FMP config must not silently default to the shared demo key',
|
||||||
|
);
|
||||||
|
assertSourceIncludes(
|
||||||
|
"apiKey.toLowerCase() === 'demo'",
|
||||||
|
'FMP routes must reject the shared demo key explicitly',
|
||||||
|
);
|
||||||
|
assertSourceIncludes(
|
||||||
|
'FMP_API_KEY is required for research and screener endpoints',
|
||||||
|
'FMP routes must surface an explicit missing-key error',
|
||||||
|
);
|
||||||
|
|
||||||
const fetchFmpCalls = apiServerSource.match(/fetchFmpJson\(url\)/g) ?? [];
|
const fetchFmpCalls = apiServerSource.match(/fetchFmpJson\(url\)/g) ?? [];
|
||||||
assert.equal(fetchFmpCalls.length, 4, 'each FMP route must fetch through fetchFmpJson(url)');
|
assert.equal(fetchFmpCalls.length, 4, 'each FMP route must fetch through fetchFmpJson(url)');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user