penny-pincher 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Penny Pincher contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # Penny Pincher
2
+
3
+ <p align="center">
4
+ <img src="public/og-image.png" alt="Penny Pincher — OSS personal finance CLI. npx penny-pincher" width="720" />
5
+ </p>
6
+
7
+ Penny Pincher is an agent-friendly CLI for connecting a bank account with Plaid and reading account data as JSON.
8
+
9
+ ```sh
10
+ npx -p penny-pincher penny-pincher auth
11
+ npx -p penny-pincher penny-pincher accounts
12
+ npx -p penny-pincher penny-pincher balances
13
+ npx -p penny-pincher penny-pincher transactions --days 30
14
+ ```
15
+
16
+ ## Setup
17
+
18
+ The default CLI flow uses the hosted Penny Pincher backend:
19
+
20
+ ```sh
21
+ npx -p penny-pincher penny-pincher auth
22
+ ```
23
+
24
+ The backend creates Plaid Link tokens, exchanges public tokens, and proxies Plaid data requests. The CLI stores an encrypted token envelope and a local signing key at `~/.penny-pincher/config.json`.
25
+
26
+ If you deploy your own backend, point the CLI at it:
27
+
28
+ ```sh
29
+ export PENNY_PINCHER_API_URL=https://your-vercel-app.vercel.app
30
+ npx -p penny-pincher penny-pincher auth
31
+ ```
32
+
33
+ Production Plaid is the default for the hosted backend. For sandbox testing, pass `--env sandbox`:
34
+
35
+ ```sh
36
+ npx -p penny-pincher penny-pincher auth --env sandbox
37
+ ```
38
+
39
+ ## Commands
40
+
41
+ - `penny-pincher auth` opens Plaid Link, exchanges the public token through the backend, and saves local token metadata.
42
+ - `penny-pincher accounts` prints linked accounts.
43
+ - `penny-pincher balances` prints accounts with balances.
44
+ - `penny-pincher transactions --days 30` prints recent transactions.
45
+ - `penny-pincher identity` prints account owner identity data when the product is enabled.
46
+ - `penny-pincher numbers` prints ACH/routing data when the Plaid `auth` product is enabled.
47
+ - `penny-pincher status` prints local connection metadata without exposing the access token.
48
+ - `penny-pincher logout` removes the saved local token.
49
+
50
+ All data commands print JSON so another agent or script can parse them directly.
51
+
52
+ ## Security Notes
53
+
54
+ The hosted backend stores your Plaid app credentials in Vercel environment variables. It does not need to store per-user Plaid access tokens. Instead, it returns an encrypted token envelope to the CLI. Data commands send that envelope back with a signed request; the backend decrypts the envelope just long enough to call Plaid.
55
+
56
+ Penny Pincher stores the encrypted envelope and a local private signing key in `~/.penny-pincher/config.json` with `0600` file permissions. Treat that file like a password. If someone steals the full file, they can query data until you revoke the Plaid Item or rotate backend encryption keys.
57
+
58
+ ## Vercel Backend
59
+
60
+ Deploy this repository to Vercel and set:
61
+
62
+ ```sh
63
+ PLAID_CLIENT_ID=your-client-id
64
+ PLAID_SECRET=your-secret
65
+ PLAID_SANDBOX_SECRET=your-sandbox-secret
66
+ PLAID_ENV=production
67
+ PLAID_REDIRECT_URI=https://penny-pincher-cli.vercel.app/oauth-return
68
+ PENNY_PINCHER_ENCRYPTION_KEY=at-least-32-random-bytes
69
+ PENNY_PINCHER_TOKEN_KEY_VERSION=v1
70
+ ```
71
+
72
+ Generate a strong encryption key with:
73
+
74
+ ```sh
75
+ openssl rand -base64 32
76
+ ```
77
+
78
+ The Vercel API exposes:
79
+
80
+ - `POST /api/link-token`
81
+ - `POST /api/exchange`
82
+ - `POST /api/accounts`
83
+ - `POST /api/balances`
84
+ - `POST /api/transactions`
85
+ - `POST /api/identity`
86
+ - `POST /api/numbers`
87
+
88
+ ## Bring Your Own Plaid App
89
+
90
+ You can still run the CLI without the hosted broker by using local Plaid credentials:
91
+
92
+ ```sh
93
+ export PLAID_CLIENT_ID=your-client-id
94
+ export PLAID_SECRET=your-secret
95
+ export PLAID_ENV=sandbox
96
+ npx -p penny-pincher penny-pincher auth --direct-plaid
97
+ ```
98
+
99
+ ## Development
100
+
101
+ ```sh
102
+ npm install
103
+ npm run typecheck
104
+ npm run build
105
+ npm run dev -- status
106
+ ```
107
+
108
+ Publishing is intentionally left to the package owner:
109
+
110
+ ```sh
111
+ npm login
112
+ npm publish
113
+ ```
package/dist/auth.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { type PennyPincherConfig, type PlaidEnvironment } from "./config.js";
2
+ export interface AuthOptions {
3
+ environment: PlaidEnvironment;
4
+ products: string[];
5
+ countryCodes: string[];
6
+ port: number;
7
+ openBrowser: boolean;
8
+ directPlaid: boolean;
9
+ backendUrl?: string;
10
+ onReady?: (url: string) => void;
11
+ }
12
+ export declare function runAuthFlow(options: AuthOptions): Promise<PennyPincherConfig>;
package/dist/auth.js ADDED
@@ -0,0 +1,488 @@
1
+ import express from "express";
2
+ import open from "open";
3
+ import { createHostedLinkToken, defaultBackendUrl, exchangeHostedPublicToken, normalizeBackendUrl } from "./backend.js";
4
+ import { loadConfig, saveConfig } from "./config.js";
5
+ import { generateSigningKeyPair } from "./crypto.js";
6
+ import { createPlaidClient } from "./plaid.js";
7
+ export async function runAuthFlow(options) {
8
+ if (!options.directPlaid) {
9
+ return runHostedAuthFlow(options);
10
+ }
11
+ return runDirectAuthFlow(options);
12
+ }
13
+ async function runHostedAuthFlow(options) {
14
+ const existingConfig = await loadConfig();
15
+ const keyPair = existingConfig.publicKeyPem && existingConfig.privateKeyPem
16
+ ? {
17
+ publicKeyPem: existingConfig.publicKeyPem,
18
+ privateKeyPem: existingConfig.privateKeyPem
19
+ }
20
+ : generateSigningKeyPair();
21
+ const backendUrl = normalizeBackendUrl(options.backendUrl
22
+ ?? process.env.PENNY_PINCHER_API_URL
23
+ ?? process.env.PENNY_PINCER_API_URL
24
+ ?? process.env.FINCLAW_API_URL
25
+ ?? defaultBackendUrl);
26
+ const redirectUri = new URL("/oauth-return", backendUrl).toString();
27
+ const link = await createHostedLinkToken(backendUrl, {
28
+ publicKeyPem: keyPair.publicKeyPem,
29
+ environment: options.environment,
30
+ products: options.products,
31
+ countryCodes: options.countryCodes,
32
+ redirectUri
33
+ });
34
+ return runLocalLinkFlow({
35
+ ...options,
36
+ hostedLinkUrl: createHostedLinkUrl(backendUrl, link.linkToken, options.port),
37
+ linkToken: link.linkToken,
38
+ exchange: async (publicToken, metadata) => {
39
+ const exchange = await exchangeHostedPublicToken(backendUrl, {
40
+ publicToken,
41
+ publicKeyPem: keyPair.publicKeyPem,
42
+ environment: options.environment,
43
+ products: options.products,
44
+ countryCodes: options.countryCodes,
45
+ metadata
46
+ }, keyPair.privateKeyPem);
47
+ const config = {
48
+ mode: "hosted",
49
+ environment: exchange.environment,
50
+ backendUrl,
51
+ tokenEnvelope: exchange.tokenEnvelope,
52
+ publicKeyPem: keyPair.publicKeyPem,
53
+ privateKeyPem: keyPair.privateKeyPem,
54
+ itemId: exchange.itemId,
55
+ institutionName: exchange.institutionName,
56
+ institutionId: exchange.institutionId,
57
+ products: exchange.products,
58
+ countryCodes: exchange.countryCodes
59
+ };
60
+ await saveConfig(config);
61
+ return config;
62
+ }
63
+ });
64
+ }
65
+ async function runDirectAuthFlow(options) {
66
+ const client = createPlaidClient(options.environment);
67
+ const redirectUri = process.env.PLAID_REDIRECT_URI;
68
+ const linkTokenResponse = await client.linkTokenCreate({
69
+ user: {
70
+ client_user_id: `penny-pincher-${Date.now()}`
71
+ },
72
+ client_name: "Penny Pincher",
73
+ products: options.products,
74
+ country_codes: options.countryCodes,
75
+ language: "en",
76
+ redirect_uri: redirectUri
77
+ });
78
+ const linkToken = linkTokenResponse.data.link_token;
79
+ return runLocalLinkFlow({
80
+ ...options,
81
+ linkToken,
82
+ exchange: async (publicToken, metadata) => {
83
+ const exchange = await client.itemPublicTokenExchange({
84
+ public_token: publicToken
85
+ });
86
+ const config = {
87
+ mode: "direct",
88
+ environment: options.environment,
89
+ accessToken: exchange.data.access_token,
90
+ itemId: exchange.data.item_id,
91
+ institutionName: metadata?.institution?.name,
92
+ institutionId: metadata?.institution?.institution_id,
93
+ products: options.products,
94
+ countryCodes: options.countryCodes
95
+ };
96
+ await saveConfig(config);
97
+ return config;
98
+ }
99
+ });
100
+ }
101
+ async function runLocalLinkFlow(options) {
102
+ const app = express();
103
+ app.use(express.json());
104
+ app.use((_request, response, next) => {
105
+ response.setHeader("access-control-allow-origin", "*");
106
+ response.setHeader("access-control-allow-methods", "GET,POST,OPTIONS");
107
+ response.setHeader("access-control-allow-headers", "content-type");
108
+ if (_request.method === "OPTIONS") {
109
+ response.status(204).end();
110
+ return;
111
+ }
112
+ next();
113
+ });
114
+ let server;
115
+ const finished = new Promise((resolve, reject) => {
116
+ const timeout = setTimeout(() => {
117
+ reject(new Error("Timed out waiting for Plaid Link to finish."));
118
+ }, 10 * 60 * 1000);
119
+ app.get("/", (_request, response) => {
120
+ if (options.hostedLinkUrl) {
121
+ response.type("html").send(renderHostedWaitingPage(options.hostedLinkUrl));
122
+ return;
123
+ }
124
+ response.type("html").send(renderLinkPage(options.linkToken));
125
+ });
126
+ app.get("/oauth-return", (_request, response) => {
127
+ response.type("html").send(renderLinkPage(options.linkToken));
128
+ });
129
+ app.post("/exchange", async (request, response) => {
130
+ try {
131
+ const publicToken = request.body?.public_token;
132
+ const metadata = request.body?.metadata;
133
+ if (!publicToken || typeof publicToken !== "string") {
134
+ response.status(400).json({ error: "Missing public_token" });
135
+ return;
136
+ }
137
+ const config = await options.exchange(publicToken, metadata);
138
+ response.json({
139
+ ok: true,
140
+ configPath: "~/.penny-pincher/config.json",
141
+ institutionName: config.institutionName,
142
+ environment: config.environment,
143
+ mode: config.mode
144
+ });
145
+ clearTimeout(timeout);
146
+ resolve(config);
147
+ }
148
+ catch (error) {
149
+ clearTimeout(timeout);
150
+ reject(error);
151
+ response.status(500).json({
152
+ error: error instanceof Error ? error.message : "Unknown Plaid exchange error"
153
+ });
154
+ }
155
+ });
156
+ }).finally(() => {
157
+ server?.close();
158
+ });
159
+ server = await listen(app, options.port);
160
+ const url = options.hostedLinkUrl ?? `http://localhost:${options.port}`;
161
+ options.onReady?.(url);
162
+ if (options.openBrowser) {
163
+ await open(url);
164
+ }
165
+ return finished;
166
+ }
167
+ function createHostedLinkUrl(backendUrl, linkToken, port) {
168
+ const url = new URL("/connect", backendUrl);
169
+ url.searchParams.set("link_token", linkToken);
170
+ url.searchParams.set("callback", `http://localhost:${port}/exchange`);
171
+ return url.toString();
172
+ }
173
+ function listen(app, port) {
174
+ return new Promise((resolve, reject) => {
175
+ const server = app.listen(port, () => resolve(server));
176
+ server.on("error", reject);
177
+ });
178
+ }
179
+ const SHARED_AUTH_STYLES = `
180
+ :root {
181
+ --paper:#fff; --lattice:#fbf4ec; --bloom:#f0cfab; --pulse:#d08454;
182
+ --spark:#e04a14; --sovereign:#142a5c; --deep:#08163c;
183
+ --glyph:#0b0f22; --stone:#757e96;
184
+ }
185
+ html, body { background: var(--paper); color: var(--glyph); margin: 0;
186
+ font-family: "Inter", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
187
+ -webkit-font-smoothing: antialiased; }
188
+ #ca-bg { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; opacity: 0.30; }
189
+ .stage { position: relative; z-index: 1; min-height: 100vh; display: flex; flex-direction: column; }
190
+ .top-bar { border-bottom: 1px solid var(--glyph); }
191
+ .top-bar-inner { max-width: 760px; margin: 0 auto; padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; gap: 16px; }
192
+ .brand { display: inline-flex; align-items: center; gap: 10px; text-decoration: none; color: var(--glyph); }
193
+ .brand-mark { font-family: "JetBrains Mono", monospace; text-transform: uppercase; letter-spacing: 0.16em; font-size: 11px; font-weight: 700; }
194
+ .chip { display: inline-flex; align-items: center; gap: 8px; padding: 5px 10px; border: 1px solid var(--glyph); border-radius: 2px; font-family: "JetBrains Mono", monospace; font-size: 10px; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--glyph); background: var(--paper); }
195
+ .chip::before { content: "▪"; color: var(--spark); }
196
+ main { flex: 1; max-width: 760px; width: 100%; margin: 0 auto; padding: 64px 24px 56px; box-sizing: border-box; }
197
+ .label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
198
+ .label { font-family: "Inter", sans-serif; font-weight: 500; font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--glyph); }
199
+ .label::before { content: "▪"; color: var(--spark); margin-right: 0.5em; }
200
+ .label-aux { font-family: "JetBrains Mono", monospace; text-transform: uppercase; letter-spacing: 0.16em; font-size: 10px; color: var(--stone); }
201
+ .display-lift { font-family: "Archivo Black", "Helvetica Neue", Arial, sans-serif; font-weight: 900; letter-spacing: -0.025em; line-height: 0.9; color: var(--glyph); font-size: clamp(3.5rem, 14vw, 9rem); text-shadow: 5px 5px 0 var(--bloom), 10px 10px 0 rgba(8, 22, 60, 0.22); margin: 0; }
202
+ .lede { max-width: 560px; margin: 32px 0 0; font-size: 17px; line-height: 1.55; color: var(--glyph); font-weight: 500; }
203
+ .lede .dim { color: var(--stone); }
204
+ .actions { margin-top: 28px; display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
205
+ .btn-primary { background: linear-gradient(180deg, #142a5c 0%, #08163c 100%); color: #fff; padding: 13px 22px; border: 0; border-radius: 2px; font-family: "JetBrains Mono", monospace; text-transform: uppercase; letter-spacing: 0.16em; font-size: 12px; font-weight: 700; display: inline-flex; align-items: center; gap: 10px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 2px 0 var(--bloom); transition: transform 80ms ease, box-shadow 80ms ease, background 80ms ease; cursor: pointer; text-decoration: none; }
206
+ .btn-primary:hover { background: linear-gradient(180deg, #1f3d7f 0%, #0f2050 100%); }
207
+ .btn-primary:active { transform: translateY(2px); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18); }
208
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
209
+ .status { display: inline-flex; align-items: center; gap: 10px; padding: 9px 14px; border: 1px solid var(--glyph); border-radius: 2px; background: var(--paper); font-family: "JetBrains Mono", monospace; font-size: 10.5px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--glyph); }
210
+ .status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 1px; background: var(--spark); animation: pulse 1.4s ease-in-out infinite; }
211
+ .status.is-success .dot { background: var(--sovereign); animation: none; }
212
+ .status.is-error .dot { background: var(--spark); animation: none; }
213
+ .status.is-idle .dot { background: var(--stone); animation: none; }
214
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
215
+ @media (prefers-reduced-motion: reduce) { .status .dot { animation: none; } }
216
+ .telemetry { margin-top: 40px; padding-top: 22px; border-top: 1px solid rgba(11, 15, 34, 0.15); display: flex; gap: 28px; flex-wrap: wrap; font-family: "JetBrains Mono", monospace; font-size: 10.5px; letter-spacing: 0.16em; text-transform: uppercase; }
217
+ .telemetry .k { color: var(--stone); }
218
+ .telemetry .v { color: var(--glyph); font-weight: 700; margin-left: 8px; }
219
+ .ink { margin-top: 28px; background: var(--glyph); border: 1.5px solid var(--glyph); border-radius: 2px; box-shadow: 4px 4px 0 var(--sovereign); padding: 16px 18px; }
220
+ .ink .ink-label { font-family: "JetBrains Mono", monospace; text-transform: uppercase; letter-spacing: 0.16em; font-size: 10px; font-weight: 700; color: var(--spark); margin-bottom: 8px; }
221
+ .ink pre { margin: 0; font-family: "JetBrains Mono", monospace; font-size: 12.5px; line-height: 1.6; color: var(--bloom); white-space: pre-wrap; word-break: break-word; max-height: 280px; overflow: auto; }
222
+ footer { border-top: 1px solid var(--glyph); }
223
+ .footer-inner { max-width: 760px; margin: 0 auto; padding: 18px 24px; display: flex; gap: 12px; justify-content: space-between; align-items: center; flex-wrap: wrap; font-family: "JetBrains Mono", monospace; text-transform: uppercase; letter-spacing: 0.16em; font-size: 10px; color: var(--stone); }
224
+ .note { margin-top: 16px; font-family: "JetBrains Mono", monospace; text-transform: uppercase; letter-spacing: 0.16em; font-size: 10px; color: var(--stone); }
225
+ .note .accent { color: var(--spark); }
226
+ `;
227
+ const FONT_LINKS = `
228
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
229
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
230
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" />
231
+ `;
232
+ const BRAND_SVG = `
233
+ <svg viewBox="0 0 36 36" width="22" height="22" aria-hidden="true">
234
+ <g fill="#F0CFAB">
235
+ <rect x="0" y="0" width="8" height="8" rx="1"/><rect x="9" y="0" width="8" height="8" rx="1"/>
236
+ <rect x="27" y="0" width="8" height="8" rx="1"/><rect x="18" y="9" width="8" height="8" rx="1"/>
237
+ <rect x="0" y="18" width="8" height="8" rx="1"/><rect x="18" y="18" width="8" height="8" rx="1"/>
238
+ <rect x="27" y="18" width="8" height="8" rx="1"/><rect x="9" y="27" width="8" height="8" rx="1"/>
239
+ <rect x="18" y="27" width="8" height="8" rx="1"/>
240
+ </g>
241
+ <g fill="#E04A14">
242
+ <rect x="18" y="0" width="8" height="8" rx="1"/><rect x="9" y="9" width="8" height="8" rx="1"/>
243
+ <rect x="9" y="18" width="8" height="8" rx="1"/><rect x="0" y="27" width="8" height="8" rx="1"/>
244
+ <rect x="27" y="27" width="8" height="8" rx="1"/>
245
+ </g>
246
+ </svg>
247
+ `;
248
+ const CA_BG_SCRIPT = `
249
+ (function () {
250
+ const canvas = document.getElementById("ca-bg");
251
+ if (!canvas) return;
252
+ const ctx = canvas.getContext("2d", { alpha: true });
253
+ const dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
254
+ const CELL_SIZE = 44, CELL_GAP = 4, STEP_MS = 1200;
255
+ const EASE_IN = 0.032, EASE_OUT = 0.020, MAX_ALPHA = 0.72;
256
+ const CELL_RGB = [195, 210, 240];
257
+ let cols = 0, rows = 0;
258
+ let grid = new Uint8Array(0), alpha = new Float32Array(0), target = new Float32Array(0);
259
+ let generation = 0, lastStep = 0;
260
+ const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
261
+ function resize() {
262
+ const w = window.innerWidth, h = window.innerHeight;
263
+ canvas.width = Math.floor(w * dpr); canvas.height = Math.floor(h * dpr);
264
+ canvas.style.width = w + "px"; canvas.style.height = h + "px";
265
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
266
+ const pitch = CELL_SIZE + CELL_GAP;
267
+ cols = Math.ceil(w / pitch) + 1; rows = Math.ceil(h / pitch) + 1;
268
+ const n = cols * rows;
269
+ grid = new Uint8Array(n); alpha = new Float32Array(n); target = new Float32Array(n);
270
+ for (let i = 0; i < n; i++) if (Math.random() < 0.22) grid[i] = 1;
271
+ for (let i = 0; i < n; i++) target[i] = grid[i] ? MAX_ALPHA : 0;
272
+ }
273
+ function idx(x, y) { return ((y + rows) % rows) * cols + ((x + cols) % cols); }
274
+ function step() {
275
+ const next = new Uint8Array(grid.length);
276
+ for (let y = 0; y < rows; y++) for (let x = 0; x < cols; x++) {
277
+ let n = 0;
278
+ for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
279
+ if (dx === 0 && dy === 0) continue;
280
+ n += grid[idx(x + dx, y + dy)];
281
+ }
282
+ const here = grid[idx(x, y)];
283
+ if (here && (n === 2 || n === 3)) next[idx(x, y)] = 1;
284
+ else if (!here && n === 3) next[idx(x, y)] = 1;
285
+ }
286
+ grid = next; generation++;
287
+ if (generation % 80 === 0) for (let i = 0; i < grid.length; i++) if (Math.random() < 0.05) grid[i] = 1;
288
+ if (generation % 320 === 0) for (let i = 0; i < grid.length; i++) if (Math.random() < 0.24) grid[i] = 1;
289
+ for (let i = 0; i < grid.length; i++) target[i] = grid[i] ? MAX_ALPHA : 0;
290
+ }
291
+ function draw() {
292
+ const w = canvas.width / dpr, h = canvas.height / dpr;
293
+ ctx.clearRect(0, 0, w, h);
294
+ const pitch = CELL_SIZE + CELL_GAP;
295
+ for (let y = 0; y < rows; y++) for (let x = 0; x < cols; x++) {
296
+ const a = alpha[y * cols + x];
297
+ if (a <= 0.003) continue;
298
+ ctx.fillStyle = "rgba(" + CELL_RGB[0] + "," + CELL_RGB[1] + "," + CELL_RGB[2] + "," + a + ")";
299
+ ctx.fillRect(x * pitch, y * pitch, CELL_SIZE, CELL_SIZE);
300
+ }
301
+ }
302
+ function frame(now) {
303
+ if (!lastStep) lastStep = now;
304
+ if (now - lastStep >= STEP_MS) { step(); lastStep = now; }
305
+ let dirty = false;
306
+ for (let i = 0; i < alpha.length; i++) {
307
+ const t = target[i], a = alpha[i];
308
+ if (a !== t) {
309
+ const ease = t > a ? EASE_IN : EASE_OUT;
310
+ const nxt = a + (t - a) * ease;
311
+ alpha[i] = Math.abs(nxt - t) < 0.002 ? t : nxt;
312
+ dirty = true;
313
+ }
314
+ }
315
+ if (dirty) draw();
316
+ requestAnimationFrame(frame);
317
+ }
318
+ resize();
319
+ let raf = 0;
320
+ window.addEventListener("resize", () => { if (raf) cancelAnimationFrame(raf); raf = requestAnimationFrame(resize); });
321
+ if (reducedMotion) { for (let i = 0; i < alpha.length; i++) alpha[i] = target[i]; draw(); }
322
+ else { for (let i = 0; i < alpha.length; i++) alpha[i] = 0; requestAnimationFrame(frame); }
323
+ })();
324
+ `;
325
+ function renderLinkPage(linkToken) {
326
+ const serializedLinkToken = JSON.stringify(linkToken);
327
+ return `<!doctype html>
328
+ <html lang="en">
329
+ <head>
330
+ <meta charset="utf-8" />
331
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
332
+ <title>Penny-Pincher · Connect bank</title>
333
+ ${FONT_LINKS}
334
+ <script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>
335
+ <style>${SHARED_AUTH_STYLES}</style>
336
+ </head>
337
+ <body>
338
+ <canvas id="ca-bg" aria-hidden="true"></canvas>
339
+ <div class="stage">
340
+ <header class="top-bar">
341
+ <div class="top-bar-inner">
342
+ <a href="/" class="brand">${BRAND_SVG}<span class="brand-mark">Penny-Pincher</span></a>
343
+ <span class="chip">Plaid Link · Direct</span>
344
+ </div>
345
+ </header>
346
+ <main>
347
+ <div class="label-row">
348
+ <span class="label">§ Auth · step 01 / 02</span>
349
+ <span class="label-aux">handshake · plaid → cli</span>
350
+ </div>
351
+ <h1 class="display-lift">CONNECT</h1>
352
+ <p class="lede">
353
+ Plaid Link should open in a moment.
354
+ <span class="dim">Pick a bank, sign in, and the CLI will pick up the token automatically. You can close this tab when it's done.</span>
355
+ </p>
356
+ <div class="actions">
357
+ <button type="button" id="open" class="btn-primary">Open Plaid Link →</button>
358
+ <span id="status" class="status"><span class="dot"></span><span id="status-text">Initialising</span></span>
359
+ </div>
360
+ <div class="telemetry">
361
+ <span><span class="k">Mode</span><span class="v">Direct · local Plaid creds</span></span>
362
+ <span><span class="k">Tokens</span><span class="v">never persisted</span></span>
363
+ <span><span class="k">Transport</span><span class="v">localhost · loopback</span></span>
364
+ </div>
365
+ <div id="ink" class="ink" hidden>
366
+ <div class="ink-label" id="ink-label">Diagnostic</div>
367
+ <pre id="ink-body"></pre>
368
+ </div>
369
+ <p class="note">
370
+ <span class="accent">▪</span> Trouble? Press <code style="text-transform:none;letter-spacing:0;color:var(--glyph);font-weight:700;">Ctrl-C</code> in your terminal and re-run
371
+ <code style="text-transform:none;letter-spacing:0;color:var(--glyph);font-weight:700;">penny-pincher auth --direct-plaid</code>.
372
+ </p>
373
+ </main>
374
+ <footer>
375
+ <div class="footer-inner">
376
+ <span>▪ Penny-Pincher · navy &amp; ember</span>
377
+ <span>localhost · plaid direct mode</span>
378
+ </div>
379
+ </footer>
380
+ </div>
381
+ <script>
382
+ const statusEl = document.getElementById("status");
383
+ const statusTxt = document.getElementById("status-text");
384
+ const openBtn = document.getElementById("open");
385
+ const ink = document.getElementById("ink");
386
+ const inkLabel = document.getElementById("ink-label");
387
+ const inkBody = document.getElementById("ink-body");
388
+ function setStatus(text, kind) {
389
+ statusTxt.textContent = text;
390
+ statusEl.classList.remove("is-success", "is-error", "is-idle");
391
+ if (kind) statusEl.classList.add("is-" + kind);
392
+ }
393
+ function showInk(label, body) {
394
+ inkLabel.textContent = label;
395
+ inkBody.textContent = typeof body === "string" ? body : JSON.stringify(body, null, 2);
396
+ ink.hidden = false;
397
+ }
398
+ setStatus("Opening Plaid Link", null);
399
+ const handler = Plaid.create({
400
+ token: ${serializedLinkToken},
401
+ receivedRedirectUri: window.location.pathname === "/oauth-return" ? window.location.href : undefined,
402
+ onSuccess: async (public_token, metadata) => {
403
+ setStatus("Exchanging token", null);
404
+ try {
405
+ const response = await fetch("/exchange", {
406
+ method: "POST",
407
+ headers: { "content-type": "application/json" },
408
+ body: JSON.stringify({ public_token, metadata })
409
+ });
410
+ const body = await response.json();
411
+ if (response.ok) {
412
+ setStatus("Connected · back to your terminal", "success");
413
+ } else {
414
+ setStatus("Token exchange failed", "error");
415
+ showInk("Exchange error", body);
416
+ }
417
+ } catch (err) {
418
+ setStatus("Network error", "error");
419
+ showInk("Exchange error", String(err && err.message ? err.message : err));
420
+ }
421
+ },
422
+ onExit: (error) => {
423
+ if (error) {
424
+ setStatus("Plaid Link error", "error");
425
+ showInk("Plaid onExit", error);
426
+ } else {
427
+ setStatus("Closed · click to reopen", "idle");
428
+ }
429
+ }
430
+ });
431
+ openBtn.addEventListener("click", () => handler.open());
432
+ handler.open();
433
+ </script>
434
+ <script>${CA_BG_SCRIPT}</script>
435
+ </body>
436
+ </html>`;
437
+ }
438
+ function renderHostedWaitingPage(hostedLinkUrl) {
439
+ const serializedHostedLinkUrl = JSON.stringify(hostedLinkUrl);
440
+ return `<!doctype html>
441
+ <html lang="en">
442
+ <head>
443
+ <meta charset="utf-8" />
444
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
445
+ <title>Penny-Pincher · Redirecting to Plaid Link</title>
446
+ ${FONT_LINKS}
447
+ <style>${SHARED_AUTH_STYLES}</style>
448
+ </head>
449
+ <body>
450
+ <div class="stage">
451
+ <header class="top-bar">
452
+ <div class="top-bar-inner">
453
+ <a href="/" class="brand">${BRAND_SVG}<span class="brand-mark">Penny-Pincher</span></a>
454
+ <span class="chip">Plaid Link · Hosted</span>
455
+ </div>
456
+ </header>
457
+ <main>
458
+ <div class="label-row">
459
+ <span class="label">§ Auth · redirect</span>
460
+ <span class="label-aux">cli → hosted broker</span>
461
+ </div>
462
+ <h1 class="display-lift">REDIRECT</h1>
463
+ <p class="lede">
464
+ Sending you to the hosted Penny-Pincher Plaid Link page.
465
+ <span class="dim">Leave this local callback tab open until auth completes — your terminal will pick up the token as soon as Plaid hands it back.</span>
466
+ </p>
467
+ <div class="actions">
468
+ <a id="link" href="#" class="btn-primary">Open Plaid Link →</a>
469
+ <span class="status"><span class="dot"></span>Forwarding</span>
470
+ </div>
471
+ <p class="note"><span class="accent">▪</span> If nothing happens, use the button above.</p>
472
+ </main>
473
+ <footer>
474
+ <div class="footer-inner">
475
+ <span>▪ Penny-Pincher · navy &amp; ember</span>
476
+ <span>localhost · hosted broker mode</span>
477
+ </div>
478
+ </footer>
479
+ </div>
480
+ <script>
481
+ const url = ${serializedHostedLinkUrl};
482
+ document.getElementById("link").href = url;
483
+ window.location.href = url;
484
+ </script>
485
+ </body>
486
+ </html>`;
487
+ }
488
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}