sheetlink 0.1.2 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sheetlink",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "CLI for SheetLink — sync your bank transactions to any destination",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,8 @@ import { randomBytes } from 'crypto';
18
18
  import { writeConfig, getApiUrl } from '../config.js';
19
19
  import { getTierStatus, listItems } from '../api.js';
20
20
 
21
- const GOOGLE_CLIENT_ID = '967710910027-qq2tuel7vsi2i06h4h096hbvok8kfmhk.apps.googleusercontent.com';
21
+ const GOOGLE_CLIENT_ID = '967710910027-j88nejbs5rnjb5b4801er8sffkv4crdb.apps.googleusercontent.com';
22
+ const GOOGLE_CLIENT_SECRET = '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';
22
23
  const REDIRECT_PORT = 9876;
23
24
  const REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/callback`;
24
25
 
@@ -69,60 +70,38 @@ export async function cmdAuth(options) {
69
70
  async function googleOAuthFlow() {
70
71
  return new Promise((resolve, reject) => {
71
72
  const state = randomBytes(16).toString('hex');
72
- const nonce = randomBytes(16).toString('hex');
73
73
 
74
+ // Authorization code flow (Desktop app client)
74
75
  const params = new URLSearchParams({
75
76
  client_id: GOOGLE_CLIENT_ID,
76
77
  redirect_uri: REDIRECT_URI,
77
- response_type: 'id_token',
78
+ response_type: 'code',
78
79
  scope: 'openid email profile',
79
80
  state,
80
- nonce,
81
+ access_type: 'online',
81
82
  });
82
83
 
83
84
  const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
84
85
 
85
- // Start local server to catch the redirect
86
+ // Start local server to catch the authorization code redirect
86
87
  const server = http.createServer((req, res) => {
87
88
  const url = new URL(req.url, `http://localhost:${REDIRECT_PORT}`);
88
89
 
89
- // Google returns id_token in the fragment (#) which is client-side only.
90
- // We serve a tiny page that POSTs it to our server.
91
90
  if (url.pathname === '/callback') {
91
+ const code = url.searchParams.get('code');
92
+ const returnedState = url.searchParams.get('state');
93
+ const error = url.searchParams.get('error');
94
+
92
95
  res.writeHead(200, { 'Content-Type': 'text/html' });
93
- res.end(`<!DOCTYPE html>
94
- <html>
95
- <body>
96
- <script>
97
- const params = new URLSearchParams(location.hash.slice(1));
98
- fetch('/token', {
99
- method: 'POST',
100
- headers: {'Content-Type': 'application/json'},
101
- body: JSON.stringify({ id_token: params.get('id_token'), state: params.get('state') })
102
- }).then(() => {
103
- document.body.innerHTML = '<p>Authenticated! You can close this tab.</p>';
104
- });
105
- </script>
106
- </body>
107
- </html>`);
108
- return;
109
- }
96
+ res.end('<!DOCTYPE html><html><body><p>Authenticated! You can close this tab.</p></body></html>');
97
+ server.close();
110
98
 
111
- if (url.pathname === '/token' && req.method === 'POST') {
112
- let body = '';
113
- req.on('data', d => body += d);
114
- req.on('end', () => {
115
- res.writeHead(200);
116
- res.end('ok');
117
- server.close();
118
- try {
119
- const { id_token, state: returnedState } = JSON.parse(body);
120
- if (returnedState !== state) return reject(new Error('State mismatch — possible CSRF'));
121
- resolve(id_token);
122
- } catch (e) {
123
- reject(e);
124
- }
125
- });
99
+ if (error) return reject(new Error(`OAuth error: ${error}`));
100
+ if (returnedState !== state) return reject(new Error('State mismatch — possible CSRF'));
101
+ if (!code) return reject(new Error('No authorization code received'));
102
+
103
+ // Exchange code for id_token via token endpoint
104
+ exchangeCodeForIdToken(code).then(resolve).catch(reject);
126
105
  return;
127
106
  }
128
107
 
@@ -144,6 +123,29 @@ async function googleOAuthFlow() {
144
123
  });
145
124
  }
146
125
 
126
+ async function exchangeCodeForIdToken(code) {
127
+ const res = await fetch('https://oauth2.googleapis.com/token', {
128
+ method: 'POST',
129
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
130
+ body: new URLSearchParams({
131
+ code,
132
+ client_id: GOOGLE_CLIENT_ID,
133
+ client_secret: GOOGLE_CLIENT_SECRET,
134
+ redirect_uri: REDIRECT_URI,
135
+ grant_type: 'authorization_code',
136
+ }),
137
+ });
138
+
139
+ if (!res.ok) {
140
+ const err = await res.json().catch(() => ({}));
141
+ throw new Error(`Token exchange failed: ${err.error_description || err.error || res.statusText}`);
142
+ }
143
+
144
+ const data = await res.json();
145
+ if (!data.id_token) throw new Error('No id_token in token response');
146
+ return data.id_token;
147
+ }
148
+
147
149
  async function exchangeGoogleToken(idToken) {
148
150
  const res = await fetch(`${getApiUrl()}/auth/login`, {
149
151
  method: 'POST',
@@ -76,8 +76,8 @@ export async function cmdSync(options) {
76
76
  }
77
77
 
78
78
  if (output.startsWith('sqlite://')) {
79
- // sqlite:///absolute/path or sqlite://relative/path
80
- const dbPath = output.replace(/^sqlite:\/\/\/?/, '') || './sheetlink.db';
79
+ // sqlite:///absolute/path /absolute/path, sqlite://relative/path → relative/path
80
+ const dbPath = output.replace(/^sqlite:\/\//, '') || './sheetlink.db';
81
81
  writeSQLite(allTransactions, allAccounts, dbPath);
82
82
  return;
83
83
  }