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 +21 -0
- package/README.md +113 -0
- package/dist/auth.d.ts +12 -0
- package/dist/auth.js +488 -0
- package/dist/auth.js.map +1 -0
- package/dist/backend.d.ts +47 -0
- package/dist/backend.js +46 -0
- package/dist/backend.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +155 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.js +103 -0
- package/dist/config.js.map +1 -0
- package/dist/crypto.d.ts +38 -0
- package/dist/crypto.js +131 -0
- package/dist/crypto.js.map +1 -0
- package/dist/data.d.ts +21 -0
- package/dist/data.js +104 -0
- package/dist/data.js.map +1 -0
- package/dist/plaid.d.ts +10 -0
- package/dist/plaid.js +29 -0
- package/dist/plaid.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +242 -0
- package/dist/server.js.map +1 -0
- package/package.json +57 -0
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 & 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 & 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
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|