Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a3fd4ee
Potential fix for code scanning alert no. 3: Server-side request forgery
jamespepper81 May 27, 2026
e9652b4
Fix SSRF protection bugs: NaN/Infinity in startIndex, percent-encodin…
claude May 27, 2026
aa9297a
Harden SSRF protections: route all fetch calls through fetchJson, val…
claude May 27, 2026
560f028
Merge pull request #705 from BitSleuthAI/claude/pensive-darwin-qSFOS
jamespepper81 May 27, 2026
a838235
Fix CodeQL SSRF alert: reconstruct fetch URL from trusted constant or…
claude May 27, 2026
00f9e5d
Merge pull request #706 from BitSleuthAI/claude/upbeat-goldberg-grNwd
jamespepper81 May 27, 2026
8c1bcf8
Fix SSRF CodeQL finding: refactor fetchJson to accept structured para…
claude May 27, 2026
a0bd5c6
Merge pull request #707 from BitSleuthAI/claude/eager-feynman-FTPhH
jamespepper81 May 27, 2026
5cff33d
Potential fix for code scanning alert no. 1: Workflow does not contai…
jamespepper81 May 27, 2026
c708e96
Potential fix for code scanning alert no. 2: Workflow does not contai…
jamespepper81 May 27, 2026
ea07bf5
Merge pull request #708 from BitSleuthAI/alert-autofix-1
jamespepper81 May 27, 2026
e23737d
Merge pull request #709 from BitSleuthAI/alert-autofix-2
jamespepper81 May 27, 2026
f947b30
Fix SSRF alert: add post-construction origin guard in fetchJson
claude May 27, 2026
13b1b9e
Merge pull request #710 from BitSleuthAI/claude/hopeful-goldberg-ztVt3
jamespepper81 May 27, 2026
ca3801c
Fix SSRF CodeQL alert: sanitize address/txid inputs before URL constr…
claude May 27, 2026
e44271a
Eliminate remaining taint paths in esploraGet for CodeQL compliance
claude May 27, 2026
b79c705
Merge pull request #711 from BitSleuthAI/claude/dreamy-pascal-1SZMO
jamespepper81 May 27, 2026
283c129
Merge pull request #704 from BitSleuthAI/alert-autofix-3
jamespepper81 May 27, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
push:
branches: [main]

permissions:
contents: read

jobs:
ci:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/copilot-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ on:
# branches:
# - main

permissions:
contents: read

jobs:
test-with-environment:
runs-on: ubuntu-latest
Expand Down
36 changes: 17 additions & 19 deletions src/ai/flows/enhanced-tax-report-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
*/

import { z } from '@genkit-ai/core';
import { VALID_CURRENCIES } from '@/lib/types';
import type { WalletData, Transaction, Currency } from '@/lib/types';
import { fetchJson } from '@/lib/blockchain-api';
import { eachDayOfInterval, startOfDay, isWithinInterval, format, subDays, addDays, differenceInDays } from 'date-fns';
import {
TaxCalculator,
Expand All @@ -26,7 +28,7 @@ const EnhancedTaxReportInputSchema = z.object({
walletData: z.string().describe('JSON string of the full WalletData object.'),
startDate: z.string().describe('Start date for the report in ISO 8601 format.'),
endDate: z.string().describe('End date for the report in ISO 8601 format.'),
currency: z.string().describe('The currency for the report (e.g., "USD").'),
currency: z.enum(['USD', 'EUR', 'GBP']).describe('The currency for the report.'),
accountingMethod: z.enum(['FIFO', 'LIFO', 'HIFO', 'SPEC_ID', 'AVG_COST', 'SHARED_POOL']).optional().default('FIFO'),
jurisdiction: z.enum(['US', 'UK', 'CANADA', 'AUSTRALIA', 'GERMANY', 'OTHER']).optional().default('US'),
});
Expand Down Expand Up @@ -131,26 +133,30 @@ const EnhancedTaxReportOutputSchema = z.object({
export type EnhancedTaxReportOutput = z.infer<typeof EnhancedTaxReportOutputSchema>;

async function getDailyPrices(startDate: Date, endDate: Date, currency: Currency): Promise<Record<string, number>> {
if (!(VALID_CURRENCIES as readonly string[]).includes(currency)) {
throw new Error('Invalid currency for price lookup.');
}
const prices: Record<string, number> = {};

const today = startOfDay(new Date());
const maxAllowedStartDate = subDays(today, 364);

if (startDate < maxAllowedStartDate) {
const requestedDays = differenceInDays(endDate, startDate);
const availableDays = differenceInDays(endDate, maxAllowedStartDate);

if (requestedDays > 364) {
console.warn(`CoinGecko API: Requested ${requestedDays} days of data, but only ${availableDays} days are available due to 364-day limit.`);
}

if (endDate < maxAllowedStartDate) {
throw new Error(`CoinGecko API: Both dates are older than 364 days. Historical data is not available.`);
}
}

const finalStartDate = startDate < maxAllowedStartDate ? maxAllowedStartDate : startDate;
let currentStartDate = finalStartDate;
const currencyCode = currency.toLowerCase();

try {
while (currentStartDate <= endDate) {
Expand All @@ -162,19 +168,11 @@ async function getDailyPrices(startDate: Date, endDate: Date, currency: Currency
const fromTimestamp = Math.floor(currentStartDate.getTime() / 1000);
const toTimestamp = Math.floor(currentEndDate.getTime() / 1000) + 3600;

const url = `https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range?vs_currency=${currency.toLowerCase()}&from=${fromTimestamp}&to=${toTimestamp}`;
const headers: HeadersInit = { 'Accept': 'application/json' };
const apiKey = process.env.COINGECKO_API_KEY;
if (apiKey) {
headers['x-cg-demo-api-key'] = apiKey;
}

const response = await fetch(url, { headers, next: { revalidate: 3600 } });
if (!response.ok) {
throw new Error(`Failed to fetch historical prices. Status: ${response.status}`);
}

const data = await response.json();
const data = await fetchJson('coingecko', '/api/v3/coins/bitcoin/market_chart/range', {
vs_currency: currencyCode,
from: String(fromTimestamp),
to: String(toTimestamp),
}, {}, 3600);

for (const [timestamp, price] of data.prices) {
const dateKey = format(startOfDay(new Date(timestamp)), 'yyyy-MM-dd');
Expand Down
52 changes: 20 additions & 32 deletions src/ai/flows/tax-report-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@
*/

import { z } from '@genkit-ai/core';
import { VALID_CURRENCIES } from '@/lib/types';
import type { WalletData, Transaction, Currency, Holding } from '@/lib/types';
import { fetchJson } from '@/lib/blockchain-api';
import { eachDayOfInterval, startOfDay, isWithinInterval, format, subDays, addDays, differenceInDays } from 'date-fns';

// The input schema for the tax report flow
const TaxReportInputSchema = z.object({
walletData: z.string().describe('JSON string of the full WalletData object.'),
startDate: z.string().describe('Start date for the report in ISO 8601 format.'),
endDate: z.string().describe('End date for the report in ISO 8601 format.'),
currency: z.string().describe('The currency for the report (e.g., "USD").'),
currency: z.enum(['USD', 'EUR', 'GBP']).describe('The currency for the report.'),
});
export type TaxReportInput = z.infer<typeof TaxReportInputSchema>;

Expand Down Expand Up @@ -55,65 +57,51 @@ const TaxReportOutputSchema = z.object({
export type TaxReportOutput = z.infer<typeof TaxReportOutputSchema>;

async function getDailyPrices(startDate: Date, endDate: Date, currency: Currency): Promise<Record<string, number>> {
if (!(VALID_CURRENCIES as readonly string[]).includes(currency)) {
throw new Error('Invalid currency for price lookup.');
}
const prices: Record<string, number> = {};

// Ensure consistent date handling - use startOfDay for all date comparisons

const today = startOfDay(new Date());
// Use 364 days instead of 365 to account for inclusive date counting in CoinGecko API
const maxAllowedStartDate = subDays(today, 364);

// Check if the requested range exceeds CoinGecko's 364-day limit

if (startDate < maxAllowedStartDate) {
const requestedDays = differenceInDays(endDate, startDate);
const availableDays = differenceInDays(endDate, maxAllowedStartDate);

if (requestedDays > 364) {
console.warn(`CoinGecko API: Requested ${requestedDays} days of data, but only ${availableDays} days are available due to 364-day limit.`);
console.warn(`Data will be provided from ${format(maxAllowedStartDate, 'yyyy-MM-dd')} to ${format(endDate, 'yyyy-MM-dd')}`);
}

// If both dates are older than 364 days, throw an error

if (endDate < maxAllowedStartDate) {
throw new Error(`CoinGecko API: Both start date (${format(startDate, 'yyyy-MM-dd')}) and end date (${format(endDate, 'yyyy-MM-dd')}) are older than 364 days. Historical data is not available for this range.`);
}
}

// Strictly enforce the 364-day limit - never request data older than this

const finalStartDate = startDate < maxAllowedStartDate ? maxAllowedStartDate : startDate;

console.log(`CoinGecko API: Requesting data from ${format(finalStartDate, 'yyyy-MM-dd')} to ${format(endDate, 'yyyy-MM-dd')}`);

let currentStartDate = finalStartDate;
const currencyCode = currency.toLowerCase();

try {
while (currentStartDate <= endDate) {

let currentEndDate = addDays(currentStartDate, 90);
if (currentEndDate > endDate) {
currentEndDate = endDate;
}

const fromTimestamp = Math.floor(currentStartDate.getTime() / 1000);
// Add a small buffer to the end timestamp to ensure it's always greater, preventing API errors for single-day ranges.
const toTimestamp = Math.floor(currentEndDate.getTime() / 1000) + 3600;

const url = `https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range?vs_currency=${currency.toLowerCase()}&from=${fromTimestamp}&to=${toTimestamp}`;
const headers: HeadersInit = { 'Accept': 'application/json' };
const apiKey = process.env.COINGECKO_API_KEY;
if (apiKey) {
headers['x-cg-demo-api-key'] = apiKey;
} else {
console.warn("The CoinGecko API key is missing. Public API may have rate limits.");
}

const response = await fetch(url, { headers, next: { revalidate: 3600 } });
if (!response.ok) {
const errorBody = await response.text();
console.error(`CoinGecko request for chunk ${format(currentStartDate, 'yyyy-MM-dd')} failed with status ${response.status}:`, errorBody);
throw new Error(`Failed to fetch historical prices from CoinGecko. Status: ${response.status}`);
}

const data = await response.json();
const data = await fetchJson('coingecko', '/api/v3/coins/bitcoin/market_chart/range', {
vs_currency: currencyCode,
from: String(fromTimestamp),
to: String(toTimestamp),
}, {}, 3600);

for (const [timestamp, price] of data.prices) {
const dateKey = format(startOfDay(new Date(timestamp)), 'yyyy-MM-dd');
Expand Down
Loading
Loading