Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
labels:
- dependencies
- security

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
labels:
- ci
94 changes: 94 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
test:
name: Test
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

security-audit:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Run npm audit
run: npm audit --audit-level=high

zero-network-check:
name: Zero Network Verification
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Verify no network calls in source
run: |
echo "Checking for network API usage in source files..."
if grep -rn "fetch\|XMLHttpRequest\|WebSocket\|\.ajax\|sendBeacon\|new Request" \
--include="*.js" --include="*.html" \
--exclude-dir=node_modules --exclude-dir=.git \
--exclude="build.js" .; then
echo "::error::Network API calls detected! FirePath must remain zero-network."
exit 1
fi
echo "✓ No network calls found — privacy guarantee intact."

build:
name: Build
runs-on: ubuntu-latest
needs: [test, security-audit, zero-network-check]
steps:
- uses: actions/checkout@v4

- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Build standalone HTML
run: node build.js

- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: fire_calculator
path: fire_calculator.html
retention-days: 30
42 changes: 42 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Release

on:
push:
tags:
- 'v*'

permissions:
contents: write

jobs:
release:
name: Create Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

- name: Build standalone HTML
run: node build.js

- name: Generate checksum
run: sha256sum fire_calculator.html > fire_calculator.html.sha256

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: |
fire_calculator.html
fire_calculator.html.sha256
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
node_modules/
.env
.env.*
*.log
.DS_Store
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,13 @@ FirePath-Core is the open-source foundation. **FirePath-Pro** is a Tauri desktop

Built for and with the FIRE community.

* [r/financialindependence](https://reddit.com/r/financialindependence) — where FirePath was born from real feedback
* [r/leanfire](https://reddit.com/r/leanfire) — conservative withdrawal model users
* [r/FIRE](https://reddit.com/r/FIRE) — general FIRE planning discussion
* [r/privacy](https://reddit.com/r/privacy) — local-first, no-account tool users
* [r/selfhosted](https://reddit.com/r/selfhosted) — self-host on your own machine or Raspberry Pi


Found a bug? [Open an issue](https://github.com/sandseb123/FirePath-Core/issues). Want to add a withdrawal model? See CONTRIBUTING.md. Have a Garmin .fit file parser? We'd love a PR.

---
Expand Down
82 changes: 82 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Security Policy

## Supported Versions

| Version | Supported |
| ------- | ------------------ |
| 1.0.x | :white_check_mark: |

## Architecture & Threat Model

FirePath-Core is designed as a **local-first, zero-network** financial calculator. This architecture eliminates entire categories of security threats by design.

### What FirePath Does NOT Do

- **No network requests** — No `fetch`, `XMLHttpRequest`, `WebSocket`, or any outbound calls. Verify yourself:
```bash
grep -rn "fetch\|XMLHttpRequest\|WebSocket\|\.ajax\|sendBeacon" fire_math.js fire_calculator.html
```
- **No server-side processing** — All calculations run in the browser
- **No data storage on remote servers** — Your financial data never leaves your machine
- **No authentication or accounts** — Nothing to breach
- **No third-party analytics or tracking** — No cookies, no telemetry
- **No CDN dependencies at runtime** — Chart.js is bundled inline via `build.js`

### Attack Surface

| Vector | Risk | Mitigation |
|--------|------|------------|
| XSS via CSV import | Low | Input sanitization applied to all CSV-parsed values (fixed in `2bd6dc8`) |
| XSS via scenario rendering | Low | Template outputs are escaped before DOM insertion |
| Malicious CSV file | Low | Parser validates structure; no `eval()` or dynamic code execution |
| Supply chain (npm) | Low | Only 2 dependencies (`chart.js`, `jest`); Dependabot enabled |
| Local file tampering | Medium | Users should verify file integrity via git checksums |
| Browser extension interference | Medium | Outside project scope; users should audit extensions |

### Data Handling

- **Inputs**: All financial inputs are entered by the user and stored only in browser memory during the session
- **Assumption files** (`.fire-assumptions.json`): Contain only modeling parameters (rates, ages, allocations) — no account numbers, balances, or PII
- **CSV import**: Parsed entirely in JavaScript; raw file content is never persisted

## Reporting a Vulnerability

If you discover a security vulnerability in FirePath-Core, please report it responsibly:

1. **Email**: Open a [GitHub Security Advisory](https://github.com/sandseb123/FirePath-Core/security/advisories/new) (preferred)
2. **Alternatively**: Open a private issue on the repository

### What to Include

- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)

### Response Timeline

- **Acknowledgment**: Within 48 hours
- **Assessment**: Within 7 days
- **Fix release**: Within 30 days for confirmed vulnerabilities

### What Qualifies

- XSS or injection vulnerabilities in CSV parsing or UI rendering
- Data leakage (any code path that transmits user data externally)
- Dependency vulnerabilities with a viable exploit path
- Logic errors in financial calculations that could mislead users

### What Does NOT Qualify

- Issues requiring physical access to the user's machine
- Browser-specific bugs outside the project's control
- Social engineering attacks
- Vulnerabilities in dependencies without a demonstrated exploit in this project

## Security Best Practices for Users

1. **Download from the official repo** — Only use releases from [github.com/sandseb123/FirePath-Core](https://github.com/sandseb123/FirePath-Core)
2. **Verify file integrity** — Compare checksums after download
3. **Keep dependencies updated** — Run `npm audit` periodically if using the dev setup
4. **Use a modern browser** — Ensures latest security patches and CSP support
5. **Don't modify and re-share** — If you fork, audit your changes for security
17 changes: 9 additions & 8 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ let html = fs.readFileSync(htmlPath, 'utf-8');
const chartJs = fs.readFileSync(chartPath, 'utf-8');

const placeholder = '/* Chart.js 4.x — bundled inline for offline-first guarantee. No CDN. */';
if (!html.includes(placeholder)) {
console.error('ERROR: Placeholder not found in HTML file.');
if (html.includes(placeholder)) {
html = html.replace(placeholder, chartJs);
fs.writeFileSync(htmlPath, html);
const sizeKB = Math.round(Buffer.byteLength(html) / 1024);
console.log(`Done. fire_calculator.html is now ${sizeKB}KB with Chart.js inlined.`);
} else if (html.includes('Chart')) {
console.log('Chart.js is already inlined. Skipping.');
} else {
console.error('ERROR: Placeholder not found and Chart.js not detected in HTML file.');
process.exit(1);
}

html = html.replace(placeholder, chartJs);
fs.writeFileSync(htmlPath, html);

const sizeKB = Math.round(Buffer.byteLength(html) / 1024);
console.log(`Done. fire_calculator.html is now ${sizeKB}KB with Chart.js inlined.`);
22 changes: 16 additions & 6 deletions fire_calculator.html
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,10 @@ <h3>Saved Scenarios</h3>
const u1 = rng(), u2 = rng();
return mean + stddev * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}
function escapeHTML(str) {
if (typeof str !== 'string') return str;
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}

// ── CSV Brokerage Import Parser ──
function parseCSVLine(line) {
Expand Down Expand Up @@ -1014,9 +1018,15 @@ <h3>Saved Scenarios</h3>
if(lastFireDate !== null && ytf !== Infinity && lastFireDate !== Infinity) {
const diff = lastFireDate - ytf;
if(Math.abs(diff) >= 0.08) {
const months = Math.round(diff * 12);
if(months > 0) deltaPill = `<span class="delta-pill positive">${months} month${months!==1?'s':''} earlier ↑</span>`;
else if(months < 0) deltaPill = `<span class="delta-pill negative">${-months} month${-months!==1?'s':''} later ↓</span>`;
const totalMonths = Math.round(Math.abs(diff) * 12);
const years = Math.floor(totalMonths / 12);
const remainingMonths = totalMonths % 12;
let deltaText;
if(years > 0 && remainingMonths > 0) deltaText = `${years}y ${remainingMonths}m`;
else if(years > 0) deltaText = `${years} year${years!==1?'s':''}`;
else deltaText = `${totalMonths} month${totalMonths!==1?'s':''}`;
if(diff > 0) deltaPill = `<span class="delta-pill positive">${deltaText} earlier ↑</span>`;
else deltaPill = `<span class="delta-pill negative">${deltaText} later ↓</span>`;
}
}
lastFireDate = ytf;
Expand Down Expand Up @@ -1249,7 +1259,7 @@ <h3>What These Numbers Mean</h3>
const betterYTF = i>0 && s.yearsToFire < baseline.yearsToFire;
const betterSR = i>0 && s.successRate > baseline.successRate;
return `<div class="scenario-col">
<h4>${s.name}</h4>
<h4>${escapeHTML(s.name)}</h4>
<div class="sc-row"><span class="sc-label">FIRE #</span><span class="${i>0?(betterFN?'sc-better':'sc-worse'):''}">${fmt(s.fireNumber)}</span></div>
<div class="sc-row"><span class="sc-label">Years</span><span class="${i>0?(betterYTF?'sc-better':'sc-worse'):''}">${fmtN(s.yearsToFire)}</span></div>
<div class="sc-row"><span class="sc-label">Success</span><span class="${i>0?(betterSR?'sc-better':'sc-worse'):''}">${Math.round(s.successRate*100)}%</span></div>
Expand Down Expand Up @@ -1313,12 +1323,12 @@ <h4>${s.name}</h4>
for (let idx = 0; idx < importedAccounts.length; idx++) {
const acct = importedAccounts[idx];
html += `<div style="margin-top:8px;display:flex;align-items:center;gap:6px">`;
html += `<span style="font-size:12px;font-weight:600;color:var(--text2)">${acct.label} — $${acct.totalValue.toLocaleString()}</span>`;
html += `<span style="font-size:12px;font-weight:600;color:var(--text2)">${escapeHTML(acct.label)} — $${acct.totalValue.toLocaleString()}</span>`;
html += `<button class="csv-remove-btn" data-idx="${idx}" style="font-size:10px;background:none;border:1px solid var(--red);color:var(--red);border-radius:4px;padding:1px 6px;cursor:pointer" title="Remove this account">✕</button>`;
html += `</div>`;
html += `<div class="csv-holdings"><table><thead><tr><th>Symbol</th><th>Description</th><th>Value</th></tr></thead><tbody>`;
for (const h of acct.holdings) {
html += `<tr><td>${h.symbol}</td><td>${h.description}</td><td>$${h.value.toLocaleString(undefined, {minimumFractionDigits:2, maximumFractionDigits:2})}</td></tr>`;
html += `<tr><td>${escapeHTML(h.symbol)}</td><td>${escapeHTML(h.description)}</td><td>$${h.value.toLocaleString(undefined, {minimumFractionDigits:2, maximumFractionDigits:2})}</td></tr>`;
}
html += `</tbody></table></div>`;
}
Expand Down
12 changes: 9 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1014,9 +1014,15 @@ <h3>Saved Scenarios</h3>
if(lastFireDate !== null && ytf !== Infinity && lastFireDate !== Infinity) {
const diff = lastFireDate - ytf;
if(Math.abs(diff) >= 0.08) {
const months = Math.round(diff * 12);
if(months > 0) deltaPill = `<span class="delta-pill positive">${months} month${months!==1?'s':''} earlier ↑</span>`;
else if(months < 0) deltaPill = `<span class="delta-pill negative">${-months} month${-months!==1?'s':''} later ↓</span>`;
const totalMonths = Math.round(Math.abs(diff) * 12);
const years = Math.floor(totalMonths / 12);
const remainingMonths = totalMonths % 12;
let deltaText;
if(years > 0 && remainingMonths > 0) deltaText = `${years}y ${remainingMonths}m`;
else if(years > 0) deltaText = `${years} year${years!==1?'s':''}`;
else deltaText = `${totalMonths} month${totalMonths!==1?'s':''}`;
if(diff > 0) deltaPill = `<span class="delta-pill positive">${deltaText} earlier ↑</span>`;
else deltaPill = `<span class="delta-pill negative">${deltaText} later ↓</span>`;
}
}
lastFireDate = ytf;
Expand Down
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
{
"name": "user",
"name": "firepath-core",
"version": "1.0.0",
"description": "",
"description": "Local-first FIRE calculator — your retirement numbers never leave your machine",
"main": "fire_math.js",
"scripts": {
"test": "jest --verbose"
"test": "jest --verbose",
"build": "node build.js",
"audit:security": "npm audit --audit-level=high",
"verify:network": "! grep -rn 'fetch\\|XMLHttpRequest\\|WebSocket' --include='*.js' --include='*.html' --exclude-dir=node_modules --exclude=build.js ."
},
"keywords": [],
"author": "",
"license": "ISC",
"keywords": ["fire", "financial-independence", "retirement", "calculator", "privacy"],
"author": "sandseb123",
"license": "MIT",
"devDependencies": {
"jest": "^30.2.0"
},
Expand Down
Loading