ray-finance 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -11
- package/dist/ai/agent.js +16 -3
- package/dist/ai/context.js +6 -2
- package/dist/ai/insights.js +26 -3
- package/dist/ai/redactor.js +11 -0
- package/dist/ai/system-prompt.js +2 -2
- package/dist/ai/tools.js +4 -0
- package/dist/cli/backup.js +18 -9
- package/dist/cli/chat.js +146 -40
- package/dist/cli/format.d.ts +2 -0
- package/dist/cli/format.js +25 -0
- package/dist/cli/index.js +12 -2
- package/dist/cli/setup.js +7 -1
- package/dist/daily-sync.js +19 -4
- package/dist/db/connection.js +9 -1
- package/dist/db/encryption.js +18 -7
- package/dist/db/schema.js +6 -1
- package/dist/public/link.html +47 -24
- package/dist/public/ray-logo-dark.png +0 -0
- package/dist/queries/index.js +8 -8
- package/dist/server.js +33 -1
- package/package.json +7 -2
- package/.claude/settings.local.json +0 -16
- package/.env.example +0 -13
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -19
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -9
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -5
- package/.github/workflows/ci.yml +0 -21
- package/CHANGELOG.md +0 -16
- package/CODE_OF_CONDUCT.md +0 -31
- package/CONTRIBUTING.md +0 -41
- package/Dockerfile +0 -8
- package/SECURITY.md +0 -36
- package/SPEC.md +0 -374
- package/docker-compose.yml +0 -9
- package/site/next-env.d.ts +0 -6
- package/site/next.config.ts +0 -7
- package/site/package-lock.json +0 -1661
- package/site/package.json +0 -24
- package/site/postcss.config.mjs +0 -7
- package/site/public/ray-og.jpg +0 -0
- package/site/public/robots.txt +0 -4
- package/site/public/sitemap.xml +0 -8
- package/site/src/app/copy-command.tsx +0 -31
- package/site/src/app/globals.css +0 -87
- package/site/src/app/layout.tsx +0 -64
- package/site/src/app/page.tsx +0 -841
- package/site/src/app/pii-scramble.tsx +0 -190
- package/site/src/app/reveal.tsx +0 -29
- package/site/tsconfig.json +0 -21
- package/src/ai/agent.ts +0 -106
- package/src/ai/audit.ts +0 -11
- package/src/ai/context.ts +0 -93
- package/src/ai/insights.ts +0 -474
- package/src/ai/memory.ts +0 -21
- package/src/ai/redactor.ts +0 -102
- package/src/ai/system-prompt.ts +0 -90
- package/src/ai/tools.ts +0 -716
- package/src/alerts/index.ts +0 -123
- package/src/cli/backup.ts +0 -113
- package/src/cli/chat.ts +0 -105
- package/src/cli/commands.ts +0 -240
- package/src/cli/format.ts +0 -149
- package/src/cli/index.ts +0 -193
- package/src/cli/scheduler.ts +0 -116
- package/src/cli/setup.ts +0 -189
- package/src/config.ts +0 -81
- package/src/daily-sync.ts +0 -155
- package/src/db/connection.ts +0 -38
- package/src/db/encryption.ts +0 -29
- package/src/db/helpers.ts +0 -47
- package/src/db/schema.ts +0 -196
- package/src/index.ts +0 -3
- package/src/plaid/client.ts +0 -25
- package/src/plaid/link.ts +0 -25
- package/src/plaid/sync.ts +0 -219
- package/src/public/link.html +0 -161
- package/src/queries/index.ts +0 -586
- package/src/scoring/index.ts +0 -468
- package/src/server.ts +0 -162
- package/tsconfig.json +0 -16
- /package/{site → dist}/public/favicon.png +0 -0
package/dist/daily-sync.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { syncTransactions, syncBalances, syncInvestments, syncLiabilities, } from "./plaid/sync.js";
|
|
2
2
|
import { calculateDailyScore, checkAchievements } from "./scoring/index.js";
|
|
3
|
+
import { decryptPlaidToken } from "./db/encryption.js";
|
|
4
|
+
import { config } from "./config.js";
|
|
3
5
|
/** Run the daily sync for a single database */
|
|
4
6
|
export async function runDailySync(db) {
|
|
5
7
|
const institutions = db
|
|
@@ -14,25 +16,38 @@ export async function runDailySync(db) {
|
|
|
14
16
|
console.log(`Skipping ${inst.name} (manual entry)`);
|
|
15
17
|
continue;
|
|
16
18
|
}
|
|
19
|
+
// Decrypt the stored access token
|
|
20
|
+
let accessToken;
|
|
21
|
+
try {
|
|
22
|
+
if (!config.plaidTokenSecret) {
|
|
23
|
+
console.error(` Skipping ${inst.name}: no plaidTokenSecret configured`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
accessToken = decryptPlaidToken(inst.access_token, config.plaidTokenSecret);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
console.error(` Skipping ${inst.name}: failed to decrypt access token (wrong key or corrupt data)`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
17
32
|
const products = JSON.parse(inst.products);
|
|
18
33
|
console.log(`Syncing: ${inst.name} (${products.join(", ")})`);
|
|
19
34
|
try {
|
|
20
35
|
// Always sync balances
|
|
21
|
-
const accountCount = await syncBalances(db,
|
|
36
|
+
const accountCount = await syncBalances(db, accessToken);
|
|
22
37
|
console.log(` Accounts: ${accountCount}`);
|
|
23
38
|
// Sync transactions if available
|
|
24
39
|
if (products.includes("transactions")) {
|
|
25
|
-
const txResult = await syncTransactions(db, inst.item_id,
|
|
40
|
+
const txResult = await syncTransactions(db, inst.item_id, accessToken, inst.cursor);
|
|
26
41
|
console.log(` Transactions: +${txResult.added} ~${txResult.modified} -${txResult.removed}`);
|
|
27
42
|
}
|
|
28
43
|
// Sync investments if available
|
|
29
44
|
if (products.includes("investments")) {
|
|
30
|
-
const invResult = await syncInvestments(db,
|
|
45
|
+
const invResult = await syncInvestments(db, accessToken);
|
|
31
46
|
console.log(` Investments: ${invResult.holdings} holdings, ${invResult.securities} securities`);
|
|
32
47
|
}
|
|
33
48
|
// Sync liabilities if available
|
|
34
49
|
if (products.includes("liabilities")) {
|
|
35
|
-
await syncLiabilities(db,
|
|
50
|
+
await syncLiabilities(db, accessToken);
|
|
36
51
|
console.log(` Liabilities: synced`);
|
|
37
52
|
}
|
|
38
53
|
}
|
package/dist/db/connection.js
CHANGED
|
@@ -14,7 +14,15 @@ function openDb(dbPath, encryptionKey) {
|
|
|
14
14
|
const hexKey = Buffer.from(encryptionKey, "utf8").toString("hex");
|
|
15
15
|
db.pragma(`key="x'${hexKey}'"`);
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
// Verify the key works before proceeding
|
|
18
|
+
try {
|
|
19
|
+
db.pragma("journal_mode = WAL");
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
db.close();
|
|
23
|
+
throw new Error("Failed to open database. Wrong encryption key or corrupt database file. " +
|
|
24
|
+
"If you changed your encryption key, restore from backup or delete ~/.ray/data/finance.db to start fresh.");
|
|
25
|
+
}
|
|
18
26
|
db.pragma("foreign_keys = ON");
|
|
19
27
|
migrate(db);
|
|
20
28
|
try {
|
package/dist/db/encryption.js
CHANGED
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
|
|
2
|
-
const SCRYPT_SALT = "ray-finance-plaid-token"; // Static salt is fine — secret is already high-entropy
|
|
3
2
|
const SCRYPT_KEYLEN = 32;
|
|
3
|
+
const SALT_LEN = 16;
|
|
4
4
|
export function generateKey() {
|
|
5
5
|
return randomBytes(32).toString("hex");
|
|
6
6
|
}
|
|
7
|
-
function deriveKey(secret) {
|
|
8
|
-
return scryptSync(secret,
|
|
7
|
+
function deriveKey(secret, salt) {
|
|
8
|
+
return scryptSync(secret, salt, SCRYPT_KEYLEN);
|
|
9
9
|
}
|
|
10
10
|
export function encryptPlaidToken(token, secret) {
|
|
11
|
-
const
|
|
11
|
+
const salt = randomBytes(SALT_LEN);
|
|
12
|
+
const key = deriveKey(secret, salt);
|
|
12
13
|
const iv = randomBytes(16);
|
|
13
14
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
14
15
|
const encrypted = Buffer.concat([cipher.update(token, "utf8"), cipher.final()]);
|
|
15
16
|
const authTag = cipher.getAuthTag();
|
|
16
|
-
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
17
|
+
return `${salt.toString("hex")}:${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
17
18
|
}
|
|
18
19
|
export function decryptPlaidToken(encrypted, secret) {
|
|
19
|
-
const
|
|
20
|
-
|
|
20
|
+
const parts = encrypted.split(":");
|
|
21
|
+
// Support legacy 3-part format (static salt) and new 4-part format (random salt)
|
|
22
|
+
let salt, ivHex, authTagHex, dataHex;
|
|
23
|
+
if (parts.length === 3) {
|
|
24
|
+
salt = Buffer.from("ray-finance-plaid-token", "utf8");
|
|
25
|
+
[ivHex, authTagHex, dataHex] = parts;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
[, ivHex, authTagHex, dataHex] = parts;
|
|
29
|
+
salt = Buffer.from(parts[0], "hex");
|
|
30
|
+
}
|
|
31
|
+
const key = deriveKey(secret, salt);
|
|
21
32
|
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
|
|
22
33
|
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
23
34
|
return decipher.update(Buffer.from(dataHex, "hex")) + decipher.final("utf8");
|
package/dist/db/schema.js
CHANGED
|
@@ -100,7 +100,7 @@ export function migrate(db) {
|
|
|
100
100
|
name TEXT NOT NULL,
|
|
101
101
|
target_amount REAL NOT NULL,
|
|
102
102
|
current_amount REAL DEFAULT 0,
|
|
103
|
-
|
|
103
|
+
target_date TEXT,
|
|
104
104
|
status TEXT DEFAULT 'active'
|
|
105
105
|
);
|
|
106
106
|
|
|
@@ -191,4 +191,9 @@ export function migrate(db) {
|
|
|
191
191
|
created_at TEXT DEFAULT (datetime('now'))
|
|
192
192
|
);
|
|
193
193
|
`);
|
|
194
|
+
// Migrate: rename goals.deadline -> target_date for existing databases
|
|
195
|
+
const goalCols = db.prepare(`PRAGMA table_info(goals)`).all();
|
|
196
|
+
if (goalCols.some(c => c.name === "deadline") && !goalCols.some(c => c.name === "target_date")) {
|
|
197
|
+
db.exec(`ALTER TABLE goals RENAME COLUMN deadline TO target_date`);
|
|
198
|
+
}
|
|
194
199
|
}
|
package/dist/public/link.html
CHANGED
|
@@ -2,18 +2,15 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
6
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
7
|
-
<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@700&display=swap" rel="stylesheet">
|
|
8
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
9
|
-
<link rel="icon" href="
|
|
6
|
+
<link rel="icon" href="/favicon.png">
|
|
10
7
|
<title>Ray — Connect Account</title>
|
|
11
8
|
<style>
|
|
12
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
|
|
13
10
|
body {
|
|
14
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
15
|
-
background: #
|
|
16
|
-
color: #
|
|
11
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
12
|
+
background: #fafaf9;
|
|
13
|
+
color: #1c1917;
|
|
17
14
|
display: flex;
|
|
18
15
|
align-items: center;
|
|
19
16
|
justify-content: center;
|
|
@@ -22,36 +19,37 @@
|
|
|
22
19
|
}
|
|
23
20
|
.card {
|
|
24
21
|
background: #fff;
|
|
25
|
-
border
|
|
26
|
-
|
|
22
|
+
border: 1px solid rgba(214, 211, 209, 0.6);
|
|
23
|
+
border-radius: 16px;
|
|
24
|
+
padding: 48px 40px;
|
|
27
25
|
max-width: 400px;
|
|
28
26
|
width: 100%;
|
|
29
27
|
text-align: center;
|
|
30
|
-
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
31
28
|
}
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
.logo { margin-bottom: 32px; }
|
|
30
|
+
.logo img { height: 24px; }
|
|
31
|
+
h1 { font-size: 20px; font-weight: 600; margin-bottom: 8px; color: #1c1917; }
|
|
32
|
+
p { font-size: 15px; color: #78716c; margin-bottom: 24px; line-height: 1.6; }
|
|
34
33
|
button {
|
|
35
|
-
background: #
|
|
34
|
+
background: #1c1917;
|
|
36
35
|
color: #fff;
|
|
37
36
|
border: none;
|
|
38
37
|
padding: 14px 32px;
|
|
39
38
|
border-radius: 9999px;
|
|
40
|
-
font-size:
|
|
39
|
+
font-size: 15px;
|
|
41
40
|
font-weight: 500;
|
|
42
41
|
cursor: pointer;
|
|
43
42
|
width: 100%;
|
|
44
43
|
transition: background 0.2s;
|
|
45
44
|
}
|
|
46
|
-
button:hover { background: #
|
|
47
|
-
button:disabled { background: #
|
|
48
|
-
.success { color: #
|
|
45
|
+
button:hover { background: #292524; }
|
|
46
|
+
button:disabled { background: #d6d3d1; cursor: not-allowed; }
|
|
47
|
+
.success { color: #6ab318; font-weight: 600; }
|
|
49
48
|
.error { color: #dc3545; font-size: 14px; margin-top: 16px; }
|
|
50
|
-
.logo { font-size: 28px; font-weight: 700; margin-bottom: 24px; color: #1a1a1a; font-family: 'Geist Mono', monospace; }
|
|
51
49
|
.checkmark {
|
|
52
50
|
width: 48px;
|
|
53
51
|
height: 48px;
|
|
54
|
-
border: 2.5px solid #
|
|
52
|
+
border: 2.5px solid #6ab318;
|
|
55
53
|
border-radius: 50%;
|
|
56
54
|
display: flex;
|
|
57
55
|
align-items: center;
|
|
@@ -61,7 +59,7 @@
|
|
|
61
59
|
.checkmark svg {
|
|
62
60
|
width: 24px;
|
|
63
61
|
height: 24px;
|
|
64
|
-
stroke: #
|
|
62
|
+
stroke: #6ab318;
|
|
65
63
|
fill: none;
|
|
66
64
|
stroke-width: 2.5;
|
|
67
65
|
stroke-linecap: round;
|
|
@@ -74,12 +72,24 @@
|
|
|
74
72
|
object-fit: contain;
|
|
75
73
|
margin-bottom: 16px;
|
|
76
74
|
}
|
|
75
|
+
.spinner {
|
|
76
|
+
display: inline-block;
|
|
77
|
+
width: 20px;
|
|
78
|
+
height: 20px;
|
|
79
|
+
border: 2px solid #d6d3d1;
|
|
80
|
+
border-top-color: #78716c;
|
|
81
|
+
border-radius: 50%;
|
|
82
|
+
animation: spin 0.6s linear infinite;
|
|
83
|
+
margin-bottom: 16px;
|
|
84
|
+
}
|
|
85
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
77
86
|
</style>
|
|
78
87
|
</head>
|
|
79
88
|
<body>
|
|
80
89
|
<div class="card">
|
|
81
|
-
<div class="logo"
|
|
90
|
+
<div class="logo"><img src="/ray-logo-dark.png" alt="Ray"></div>
|
|
82
91
|
<div id="connect-view">
|
|
92
|
+
<div class="spinner"></div>
|
|
83
93
|
<p>Opening Plaid...</p>
|
|
84
94
|
<div id="error" class="error" style="display:none"></div>
|
|
85
95
|
</div>
|
|
@@ -97,13 +107,26 @@
|
|
|
97
107
|
let linkHandler = null;
|
|
98
108
|
|
|
99
109
|
async function initPlaid() {
|
|
110
|
+
// Wait for Plaid SDK to load
|
|
111
|
+
if (typeof Plaid === 'undefined') {
|
|
112
|
+
let attempts = 0;
|
|
113
|
+
await new Promise((resolve, reject) => {
|
|
114
|
+
const check = setInterval(() => {
|
|
115
|
+
if (typeof Plaid !== 'undefined') { clearInterval(check); resolve(); }
|
|
116
|
+
else if (++attempts > 50) { clearInterval(check); reject(new Error('Failed to load Plaid SDK. Check your ad blocker or network connection.')); }
|
|
117
|
+
}, 100);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
100
120
|
try {
|
|
101
121
|
const res = await fetch('/api/link-token', {
|
|
102
122
|
method: 'POST',
|
|
103
123
|
headers: { 'Content-Type': 'application/json' },
|
|
104
124
|
body: JSON.stringify({ session_id: sessionId }),
|
|
105
125
|
});
|
|
106
|
-
if (!res.ok)
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
const data = await res.json().catch(() => ({}));
|
|
128
|
+
throw new Error(data.error || 'Failed to connect. Please run "ray link" again.');
|
|
129
|
+
}
|
|
107
130
|
const { link_token } = await res.json();
|
|
108
131
|
|
|
109
132
|
linkHandler = Plaid.create({
|
|
@@ -145,7 +168,7 @@
|
|
|
145
168
|
// Auto-open Plaid Link
|
|
146
169
|
linkHandler.open();
|
|
147
170
|
} catch (e) {
|
|
148
|
-
showError('This link has expired. Please run "ray link" again.');
|
|
171
|
+
showError(e.message || 'This link has expired. Please run "ray link" again.');
|
|
149
172
|
}
|
|
150
173
|
}
|
|
151
174
|
|
|
Binary file
|
package/dist/queries/index.js
CHANGED
|
@@ -263,13 +263,13 @@ export function forecastBalance(db, accountId, months = 6) {
|
|
|
263
263
|
}
|
|
264
264
|
// --- Portfolio ---
|
|
265
265
|
export function getPortfolio(db) {
|
|
266
|
-
const rows = db.prepare(`SELECT a.name as account, s.name as security, s.
|
|
267
|
-
h.quantity, h.
|
|
268
|
-
(h.
|
|
266
|
+
const rows = db.prepare(`SELECT a.name as account, s.name as security, s.ticker,
|
|
267
|
+
h.quantity, h.value, h.cost_basis,
|
|
268
|
+
(h.value - COALESCE(h.cost_basis, h.value)) as gain_loss
|
|
269
269
|
FROM holdings h
|
|
270
270
|
JOIN accounts a ON h.account_id = a.account_id
|
|
271
271
|
LEFT JOIN securities s ON h.security_id = s.security_id
|
|
272
|
-
ORDER BY h.
|
|
272
|
+
ORDER BY h.value DESC`).all();
|
|
273
273
|
const totalValue = rows.reduce((s, r) => s + (r.value || 0), 0);
|
|
274
274
|
const totalCostBasis = rows.reduce((s, r) => s + (r.cost_basis || 0), 0);
|
|
275
275
|
return {
|
|
@@ -289,12 +289,12 @@ export function getPortfolio(db) {
|
|
|
289
289
|
}
|
|
290
290
|
// --- Investment performance ---
|
|
291
291
|
export function getInvestmentPerformance(db) {
|
|
292
|
-
const rows = db.prepare(`SELECT s.name as security, s.
|
|
293
|
-
h.
|
|
292
|
+
const rows = db.prepare(`SELECT s.name as security, s.ticker,
|
|
293
|
+
h.value, h.cost_basis
|
|
294
294
|
FROM holdings h
|
|
295
295
|
LEFT JOIN securities s ON h.security_id = s.security_id
|
|
296
|
-
WHERE h.
|
|
297
|
-
ORDER BY h.
|
|
296
|
+
WHERE h.value IS NOT NULL
|
|
297
|
+
ORDER BY h.value DESC`).all();
|
|
298
298
|
const totalValue = rows.reduce((s, r) => s + (r.value || 0), 0);
|
|
299
299
|
const totalCost = rows.reduce((s, r) => s + (r.cost_basis || r.value || 0), 0);
|
|
300
300
|
const totalReturn = totalValue - totalCost;
|
package/dist/server.js
CHANGED
|
@@ -13,9 +13,41 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
13
13
|
const __dirname = dirname(__filename);
|
|
14
14
|
// Session map for Plaid Link — maps sessionId → true (single-user, no chatId needed)
|
|
15
15
|
const linkSessions = new Map();
|
|
16
|
+
// Simple rate limiter: track request counts per IP
|
|
17
|
+
const rateLimits = new Map();
|
|
18
|
+
const RATE_LIMIT_WINDOW = 60_000; // 1 minute
|
|
19
|
+
const RATE_LIMIT_MAX = 10; // max requests per window
|
|
20
|
+
function isRateLimited(ip) {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
const entry = rateLimits.get(ip);
|
|
23
|
+
if (!entry || now > entry.resetAt) {
|
|
24
|
+
rateLimits.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW });
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
entry.count++;
|
|
28
|
+
return entry.count > RATE_LIMIT_MAX;
|
|
29
|
+
}
|
|
16
30
|
export function startLinkServer() {
|
|
17
31
|
const app = express();
|
|
18
32
|
app.use(express.json());
|
|
33
|
+
// Only allow requests from localhost origin
|
|
34
|
+
app.use((req, res, next) => {
|
|
35
|
+
const origin = req.headers.origin || req.headers.referer || "";
|
|
36
|
+
const ip = req.ip || req.socket.remoteAddress || "";
|
|
37
|
+
if (req.path.startsWith("/api/")) {
|
|
38
|
+
// Check origin for API routes
|
|
39
|
+
if (origin && !origin.startsWith(`http://localhost:${config.port}`) && !origin.startsWith(`http://127.0.0.1:${config.port}`)) {
|
|
40
|
+
res.status(403).json({ error: "Forbidden" });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Rate limit API routes
|
|
44
|
+
if (isRateLimited(ip)) {
|
|
45
|
+
res.status(429).json({ error: "Too many requests" });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
next();
|
|
50
|
+
});
|
|
19
51
|
app.use(express.static(resolve(__dirname, "public")));
|
|
20
52
|
const sessionId = randomUUID();
|
|
21
53
|
linkSessions.set(sessionId, true);
|
|
@@ -120,7 +152,7 @@ export function startLinkServer() {
|
|
|
120
152
|
res.status(500).json({ error: "Failed to link account" });
|
|
121
153
|
}
|
|
122
154
|
});
|
|
123
|
-
const server = app.listen(config.port);
|
|
155
|
+
const server = app.listen(config.port, "127.0.0.1");
|
|
124
156
|
const url = `http://localhost:${config.port}/link/${sessionId}`;
|
|
125
157
|
// Auto-expire after 30 minutes
|
|
126
158
|
const timeout = setTimeout(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ray-finance",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Local-first CLI that turns your bank data into a personal AI financial advisor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,11 +25,15 @@
|
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": ">=18"
|
|
27
27
|
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist/"
|
|
30
|
+
],
|
|
28
31
|
"bin": {
|
|
29
32
|
"ray": "./dist/cli/index.js"
|
|
30
33
|
},
|
|
31
34
|
"scripts": {
|
|
32
35
|
"build": "tsc && rm -rf dist/public && cp -r src/public dist/public",
|
|
36
|
+
"test": "vitest run",
|
|
33
37
|
"prepublishOnly": "npm run build",
|
|
34
38
|
"sync": "tsx src/daily-sync.ts"
|
|
35
39
|
},
|
|
@@ -50,6 +54,7 @@
|
|
|
50
54
|
"@types/express": "^4.17.0",
|
|
51
55
|
"@types/node": "^22.0.0",
|
|
52
56
|
"tsx": "^4.7.0",
|
|
53
|
-
"typescript": "^5.5.0"
|
|
57
|
+
"typescript": "^5.5.0",
|
|
58
|
+
"vitest": "^3.2.4"
|
|
54
59
|
}
|
|
55
60
|
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm install:*)",
|
|
5
|
-
"Bash(gh api:*)",
|
|
6
|
-
"mcp__paper__create_artboard",
|
|
7
|
-
"mcp__paper__write_html",
|
|
8
|
-
"mcp__paper__get_screenshot",
|
|
9
|
-
"mcp__paper__delete_nodes",
|
|
10
|
-
"Bash(npx next:*)",
|
|
11
|
-
"Bash(npx tsc:*)",
|
|
12
|
-
"Bash(git add:*)",
|
|
13
|
-
"Bash(git commit:*)"
|
|
14
|
-
]
|
|
15
|
-
}
|
|
16
|
-
}
|
package/.env.example
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# Anthropic API key for AI chat — https://console.anthropic.com
|
|
2
|
-
ANTHROPIC_API_KEY=
|
|
3
|
-
|
|
4
|
-
# Plaid credentials for bank sync — https://dashboard.plaid.com
|
|
5
|
-
PLAID_CLIENT_ID=
|
|
6
|
-
PLAID_SECRET=
|
|
7
|
-
PLAID_ENV=production
|
|
8
|
-
|
|
9
|
-
# Database encryption key (any strong passphrase)
|
|
10
|
-
DB_ENCRYPTION_KEY=
|
|
11
|
-
|
|
12
|
-
# Separate key for encrypting stored Plaid access tokens (any strong passphrase)
|
|
13
|
-
PLAID_TOKEN_SECRET=
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: Bug Report
|
|
3
|
-
about: Report a bug
|
|
4
|
-
labels: bug
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
**What happened?**
|
|
8
|
-
|
|
9
|
-
**What did you expect?**
|
|
10
|
-
|
|
11
|
-
**Steps to reproduce**
|
|
12
|
-
1.
|
|
13
|
-
2.
|
|
14
|
-
3.
|
|
15
|
-
|
|
16
|
-
**Environment**
|
|
17
|
-
- OS:
|
|
18
|
-
- Node.js version:
|
|
19
|
-
- Ray version (`ray --version`):
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
build:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
strategy:
|
|
13
|
-
matrix:
|
|
14
|
-
node-version: [18, 20, 22]
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v4
|
|
17
|
-
- uses: actions/setup-node@v4
|
|
18
|
-
with:
|
|
19
|
-
node-version: ${{ matrix.node-version }}
|
|
20
|
-
- run: npm install
|
|
21
|
-
- run: npm run build
|
package/CHANGELOG.md
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.2.0
|
|
4
|
-
|
|
5
|
-
Initial open source release.
|
|
6
|
-
|
|
7
|
-
- CLI interface with 13 commands (`ray`, `setup`, `sync`, `link`, `status`, `transactions`, `spending`, `budgets`, `goals`, `score`, `alerts`, `export`, `import`)
|
|
8
|
-
- AI financial advisor powered by Claude with 13+ tools
|
|
9
|
-
- Plaid integration for bank sync (checking, savings, credit cards, investments, loans)
|
|
10
|
-
- Encrypted local SQLite database (AES-256)
|
|
11
|
-
- Daily financial scoring (0-100) with streaks and 14 achievements
|
|
12
|
-
- Budget tracking with overage alerts
|
|
13
|
-
- Financial goal tracking
|
|
14
|
-
- Transaction auto-recategorization via user-defined rules
|
|
15
|
-
- Conversation memory and persistent financial context
|
|
16
|
-
- Data export/import for backup and restore
|
package/CODE_OF_CONDUCT.md
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# Code of Conduct
|
|
2
|
-
|
|
3
|
-
## Our Pledge
|
|
4
|
-
|
|
5
|
-
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
|
6
|
-
|
|
7
|
-
## Our Standards
|
|
8
|
-
|
|
9
|
-
Examples of behavior that contributes to a positive environment:
|
|
10
|
-
|
|
11
|
-
- Being respectful of differing viewpoints and experiences
|
|
12
|
-
- Giving and gracefully accepting constructive feedback
|
|
13
|
-
- Focusing on what is best for the community
|
|
14
|
-
- Showing empathy towards other community members
|
|
15
|
-
|
|
16
|
-
Examples of unacceptable behavior:
|
|
17
|
-
|
|
18
|
-
- Trolling, insulting/derogatory comments, and personal or political attacks
|
|
19
|
-
- Public or private harassment
|
|
20
|
-
- Publishing others' private information without explicit permission
|
|
21
|
-
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
|
22
|
-
|
|
23
|
-
## Enforcement
|
|
24
|
-
|
|
25
|
-
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project maintainer at clark@rayfinance.app.
|
|
26
|
-
|
|
27
|
-
All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances.
|
|
28
|
-
|
|
29
|
-
## Attribution
|
|
30
|
-
|
|
31
|
-
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
|
package/CONTRIBUTING.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# Contributing
|
|
2
|
-
|
|
3
|
-
Thanks for your interest in contributing to Ray.
|
|
4
|
-
|
|
5
|
-
## Development Setup
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
git clone https://github.com/cdinnison/ray-finance.git
|
|
9
|
-
cd ray-finance
|
|
10
|
-
npm install
|
|
11
|
-
npm run build
|
|
12
|
-
npm link
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
You'll need Plaid and Anthropic API keys to test. Copy `.env.example` to `.env` and fill in your credentials.
|
|
16
|
-
|
|
17
|
-
## Making Changes
|
|
18
|
-
|
|
19
|
-
1. Fork the repo and create a branch from `main`
|
|
20
|
-
2. Make your changes
|
|
21
|
-
3. Run `npm run build` to verify the project compiles
|
|
22
|
-
4. Open a pull request
|
|
23
|
-
|
|
24
|
-
## Code Style
|
|
25
|
-
|
|
26
|
-
- TypeScript with strict mode
|
|
27
|
-
- ES modules (`"type": "module"`)
|
|
28
|
-
- Prefer simple, direct code over abstractions
|
|
29
|
-
- No unnecessary dependencies
|
|
30
|
-
|
|
31
|
-
## Reporting Bugs
|
|
32
|
-
|
|
33
|
-
Open a GitHub issue with:
|
|
34
|
-
- What you expected to happen
|
|
35
|
-
- What actually happened
|
|
36
|
-
- Steps to reproduce
|
|
37
|
-
- Your Node.js version and OS
|
|
38
|
-
|
|
39
|
-
## Security Issues
|
|
40
|
-
|
|
41
|
-
See [SECURITY.md](SECURITY.md) for reporting security vulnerabilities. Do not open public issues for security bugs.
|
package/Dockerfile
DELETED
package/SECURITY.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# Security
|
|
2
|
-
|
|
3
|
-
## Architecture
|
|
4
|
-
|
|
5
|
-
Ray is local-first. All financial data is stored on your machine in an encrypted SQLite database. No data is sent to Ray servers because there are no Ray servers.
|
|
6
|
-
|
|
7
|
-
### Encryption
|
|
8
|
-
|
|
9
|
-
- **Database encryption**: The SQLite database is encrypted at rest using AES-256 via [SQLCipher](https://www.zetetic.net/sqlcipher/) (better-sqlite3-multiple-ciphers). The encryption key is provided during setup and stored in your local config.
|
|
10
|
-
- **Plaid token encryption**: Plaid access tokens are encrypted separately using AES-256-GCM with scrypt key derivation before being stored in the database.
|
|
11
|
-
- **File permissions**: Config and database files are created with `0600` permissions (owner read/write only).
|
|
12
|
-
|
|
13
|
-
### Data Flow
|
|
14
|
-
|
|
15
|
-
Ray makes outbound API calls to three services:
|
|
16
|
-
|
|
17
|
-
| Service | Purpose | When |
|
|
18
|
-
|---------|---------|------|
|
|
19
|
-
| Plaid | Sync bank transactions and balances | `ray sync`, `ray link` |
|
|
20
|
-
| Anthropic | AI-powered chat responses | `ray` (interactive chat) |
|
|
21
|
-
|
|
22
|
-
No telemetry, analytics, or usage data is collected or transmitted.
|
|
23
|
-
|
|
24
|
-
### PII Handling
|
|
25
|
-
|
|
26
|
-
When sending data to the Anthropic API for AI chat, Ray redacts personally identifiable information (account numbers, routing numbers) before transmission and restores it in the response for display.
|
|
27
|
-
|
|
28
|
-
## Reporting a Vulnerability
|
|
29
|
-
|
|
30
|
-
If you discover a security vulnerability, please report it responsibly:
|
|
31
|
-
|
|
32
|
-
1. **Do not** open a public GitHub issue
|
|
33
|
-
2. Email **clark@rayfinance.app** with details
|
|
34
|
-
3. Include steps to reproduce if possible
|
|
35
|
-
|
|
36
|
-
I will respond within 48 hours and work with you to address the issue before any public disclosure.
|