copyhub-cli 1.0.4 → 1.0.6
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 +11 -0
- package/package.json +1 -1
- package/src/cli.js +2 -10
- package/src/config.js +104 -22
- package/src/oauth.js +71 -7
package/README.md
CHANGED
|
@@ -214,6 +214,17 @@ Everything lives under **`~/.copyhub/`** (or `%USERPROFILE%\.copyhub` on Windows
|
|
|
214
214
|
- **macOS**: you may need to grant **Accessibility** permissions for global shortcuts.
|
|
215
215
|
- Some **`Control+Alt+…`** combinations do not register reliably on Windows; prefer alternatives suggested on the setup page.
|
|
216
216
|
|
|
217
|
+
## Troubleshooting: `invalid_client` after Google sign-in
|
|
218
|
+
|
|
219
|
+
That response comes from Google’s token endpoint when the **Client ID + Client Secret pair** does not match your OAuth client.
|
|
220
|
+
|
|
221
|
+
1. **Use a “Web application” OAuth client** (not iOS/Android/Desktop-only flows meant for different redirect rules). Add **Authorized redirect URI**: `http://127.0.0.1:19999/oauth2callback` (change the port if you use `COPYHUB_OAUTH_REDIRECT_PORT` or saved `redirectPort`).
|
|
222
|
+
2. **Where credentials come from**: If **`~/.copyhub/config.json` contains both Client ID and Secret** (wizard or `copyhub config`), CopyHub uses **that pair** for OAuth — env vars are **ignored** for those two fields until you remove them from config. If config does **not** have both, CopyHub uses **`COPYHUB_GOOGLE_CLIENT_ID` / `COPYHUB_GOOGLE_CLIENT_SECRET`** from the environment / `.env`. CopyHub never mixes env Client ID with file Secret (that mismatch triggers `invalid_client`).
|
|
223
|
+
3. **Paste cleanly**: Re-open the localhost credential wizard or edit `config.json` so ID and secret have **no extra spaces or line breaks**.
|
|
224
|
+
4. Run **`copyhub status`** and confirm **Client ID/Secret source** matches what you expect.
|
|
225
|
+
|
|
226
|
+
If Google says **“The provided client secret is invalid”**, the secret does not match that Client ID (typo, truncated paste, or an old secret after you clicked **Reset secret** in Console). Download the client JSON again from Google Cloud, paste `client_id` and `client_secret` into the CopyHub wizard, and on **Mac/Safari** clear both fields before pasting so **iCloud Keychain** does not inject a saved password into the secret field.
|
|
227
|
+
|
|
217
228
|
## License
|
|
218
229
|
|
|
219
230
|
MIT — see `package.json`.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
saveConfig,
|
|
10
10
|
loadSheetSyncTarget,
|
|
11
11
|
DEFAULT_OAUTH_REDIRECT_PORT,
|
|
12
|
-
|
|
12
|
+
describeEffectiveOAuthCredentialSource,
|
|
13
13
|
ENV_GOOGLE_CLIENT_ID,
|
|
14
14
|
ENV_GOOGLE_CLIENT_SECRET,
|
|
15
15
|
ENV_OAUTH_REDIRECT_PORT,
|
|
@@ -190,22 +190,14 @@ program
|
|
|
190
190
|
const cfg = await loadConfig();
|
|
191
191
|
const sheet = await loadSheetSyncTarget();
|
|
192
192
|
const tok = await loadTokens();
|
|
193
|
-
const src = describeOAuthCredentialSource();
|
|
194
|
-
|
|
195
193
|
if (!cfg) {
|
|
196
194
|
console.log('OAuth config: missing');
|
|
197
195
|
console.log(
|
|
198
196
|
` Set ${ENV_GOOGLE_CLIENT_ID} and ${ENV_GOOGLE_CLIENT_SECRET} in .env (see .env.example), run copyhub config, or copyhub login (browser wizard)`,
|
|
199
197
|
);
|
|
200
198
|
} else {
|
|
201
|
-
const srcLabel =
|
|
202
|
-
src === 'env'
|
|
203
|
-
? 'environment / .env'
|
|
204
|
-
: src === 'mixed'
|
|
205
|
-
? 'mixed .env + config file'
|
|
206
|
-
: CONFIG_PATH;
|
|
207
199
|
console.log('OAuth config: ok');
|
|
208
|
-
console.log(` Client ID/Secret source: ${
|
|
200
|
+
console.log(` Client ID/Secret source: ${describeEffectiveOAuthCredentialSource()}`);
|
|
209
201
|
console.log(` Callback: http://127.0.0.1:${cfg.redirectPort}/oauth2callback`);
|
|
210
202
|
}
|
|
211
203
|
|
package/src/config.js
CHANGED
|
@@ -22,6 +22,34 @@ function parseRedirectPortFromEnv() {
|
|
|
22
22
|
return n;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Strip invisible / stray characters from pasted OAuth credentials (fixes many Mac copy-paste issues).
|
|
27
|
+
* @param {unknown} raw
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
export function sanitizeOAuthCredentialInput(raw) {
|
|
31
|
+
if (raw == null) return '';
|
|
32
|
+
let s = String(raw);
|
|
33
|
+
s = s.replace(/^\uFEFF/, '');
|
|
34
|
+
s = s.replace(/[\u200b-\u200d\u2060]/g, '');
|
|
35
|
+
s = s.replace(/\r/g, '');
|
|
36
|
+
s = s.replace(/[\u00a0\u202f\u2007]/g, ' ');
|
|
37
|
+
s = s.trim();
|
|
38
|
+
if (
|
|
39
|
+
(s.startsWith('"') && s.endsWith('"')) ||
|
|
40
|
+
(s.startsWith("'") && s.endsWith("'"))
|
|
41
|
+
) {
|
|
42
|
+
s = s.slice(1, -1).trim();
|
|
43
|
+
}
|
|
44
|
+
if (
|
|
45
|
+
(s.startsWith('\u201c') && s.endsWith('\u201d')) ||
|
|
46
|
+
(s.startsWith('\u2018') && s.endsWith('\u2019'))
|
|
47
|
+
) {
|
|
48
|
+
s = s.slice(1, -1).trim();
|
|
49
|
+
}
|
|
50
|
+
return s;
|
|
51
|
+
}
|
|
52
|
+
|
|
25
53
|
/**
|
|
26
54
|
* Port for the OAuth HTTP listener (env wins, then saved config, then default).
|
|
27
55
|
* Does not require Client ID / Secret (used before credential bootstrap).
|
|
@@ -44,21 +72,44 @@ export function resolveOAuthListenPort() {
|
|
|
44
72
|
|
|
45
73
|
/** Both Client ID and Secret come from environment (or .env). */
|
|
46
74
|
export function hasOAuthCredentialsInEnv() {
|
|
47
|
-
const id = process.env[ENV_GOOGLE_CLIENT_ID]
|
|
48
|
-
const sec = process.env[ENV_GOOGLE_CLIENT_SECRET]
|
|
75
|
+
const id = sanitizeOAuthCredentialInput(process.env[ENV_GOOGLE_CLIENT_ID]);
|
|
76
|
+
const sec = sanitizeOAuthCredentialInput(process.env[ENV_GOOGLE_CLIENT_SECRET]);
|
|
49
77
|
return Boolean(id && sec);
|
|
50
78
|
}
|
|
51
79
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Matches {@link loadConfig}: saved config.json pair wins over env when both are complete.
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
export function describeEffectiveOAuthCredentialSource() {
|
|
85
|
+
let filePair = false;
|
|
86
|
+
if (existsSync(CONFIG_PATH)) {
|
|
87
|
+
try {
|
|
88
|
+
const j = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
|
89
|
+
const id =
|
|
90
|
+
typeof j.clientId === 'string' ? sanitizeOAuthCredentialInput(j.clientId) : '';
|
|
91
|
+
const sec =
|
|
92
|
+
typeof j.clientSecret === 'string' ? sanitizeOAuthCredentialInput(j.clientSecret) : '';
|
|
93
|
+
filePair = Boolean(id && sec);
|
|
94
|
+
} catch {
|
|
95
|
+
/* ignore */
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const envPair = hasOAuthCredentialsInEnv();
|
|
99
|
+
|
|
100
|
+
if (filePair) {
|
|
101
|
+
return envPair
|
|
102
|
+
? `${CONFIG_PATH} (env COPYHUB_GOOGLE_* ignored for Client ID/Secret)`
|
|
103
|
+
: CONFIG_PATH;
|
|
104
|
+
}
|
|
105
|
+
if (envPair) return 'environment / .env (COPYHUB_GOOGLE_CLIENT_ID + SECRET)';
|
|
106
|
+
return '(none)';
|
|
59
107
|
}
|
|
60
108
|
|
|
61
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* OAuth credentials: use env pair OR file pair only — never mix ID from one source with secret from another (Google returns invalid_client).
|
|
111
|
+
* @returns {Promise<CopyHubConfig | null>}
|
|
112
|
+
*/
|
|
62
113
|
export async function loadConfig() {
|
|
63
114
|
/** @type {{ clientId?: string, clientSecret?: string, redirectPort?: number }} */
|
|
64
115
|
const fromFile = {};
|
|
@@ -66,29 +117,46 @@ export async function loadConfig() {
|
|
|
66
117
|
if (existsSync(CONFIG_PATH)) {
|
|
67
118
|
const raw = await readFile(CONFIG_PATH, 'utf8');
|
|
68
119
|
const j = JSON.parse(raw);
|
|
69
|
-
if (typeof j.clientId === 'string'
|
|
70
|
-
|
|
71
|
-
fromFile.
|
|
120
|
+
if (typeof j.clientId === 'string') {
|
|
121
|
+
const id = sanitizeOAuthCredentialInput(j.clientId);
|
|
122
|
+
if (id) fromFile.clientId = id;
|
|
123
|
+
}
|
|
124
|
+
if (typeof j.clientSecret === 'string') {
|
|
125
|
+
const sec = sanitizeOAuthCredentialInput(j.clientSecret);
|
|
126
|
+
if (sec) fromFile.clientSecret = sec;
|
|
72
127
|
}
|
|
73
128
|
if (typeof j.redirectPort === 'number' && Number.isFinite(j.redirectPort)) {
|
|
74
129
|
fromFile.redirectPort = j.redirectPort;
|
|
75
130
|
}
|
|
76
131
|
}
|
|
77
132
|
|
|
78
|
-
const envId = process.env[ENV_GOOGLE_CLIENT_ID]
|
|
79
|
-
const envSecret = process.env[ENV_GOOGLE_CLIENT_SECRET]
|
|
133
|
+
const envId = sanitizeOAuthCredentialInput(process.env[ENV_GOOGLE_CLIENT_ID]);
|
|
134
|
+
const envSecret = sanitizeOAuthCredentialInput(process.env[ENV_GOOGLE_CLIENT_SECRET]);
|
|
80
135
|
const envPort = parseRedirectPortFromEnv();
|
|
81
136
|
|
|
82
|
-
const clientId = envId || fromFile.clientId;
|
|
83
|
-
const clientSecret = envSecret || fromFile.clientSecret;
|
|
84
|
-
|
|
85
137
|
const filePort =
|
|
86
138
|
typeof fromFile.redirectPort === 'number'
|
|
87
139
|
? fromFile.redirectPort
|
|
88
140
|
: DEFAULT_OAUTH_REDIRECT_PORT;
|
|
89
141
|
const redirectPort = envPort ?? filePort;
|
|
90
142
|
|
|
91
|
-
|
|
143
|
+
let clientId;
|
|
144
|
+
let clientSecret;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Prefer ~/.copyhub/config.json when it holds a full OAuth pair (wizard / copyhub config).
|
|
148
|
+
* Otherwise many machines still have COPYHUB_GOOGLE_* in shell or ~/.copyhub/.env from a template —
|
|
149
|
+
* those used to override fresh wizard credentials and caused invalid_client on the callback.
|
|
150
|
+
*/
|
|
151
|
+
if (fromFile.clientId && fromFile.clientSecret) {
|
|
152
|
+
clientId = fromFile.clientId;
|
|
153
|
+
clientSecret = fromFile.clientSecret;
|
|
154
|
+
} else if (envId && envSecret) {
|
|
155
|
+
clientId = envId;
|
|
156
|
+
clientSecret = envSecret;
|
|
157
|
+
} else {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
92
160
|
|
|
93
161
|
return { clientId, clientSecret, redirectPort };
|
|
94
162
|
}
|
|
@@ -170,6 +238,15 @@ export async function mergeConfigPartial(partial) {
|
|
|
170
238
|
const out = { ...existing, ...partial };
|
|
171
239
|
delete out.sheetTab;
|
|
172
240
|
delete out.sheetDailyPrefix;
|
|
241
|
+
if (typeof out.clientId === 'string') {
|
|
242
|
+
out.clientId = sanitizeOAuthCredentialInput(out.clientId);
|
|
243
|
+
}
|
|
244
|
+
if (typeof out.clientSecret === 'string') {
|
|
245
|
+
out.clientSecret = sanitizeOAuthCredentialInput(out.clientSecret);
|
|
246
|
+
}
|
|
247
|
+
if (typeof out.googleSheetId === 'string') {
|
|
248
|
+
out.googleSheetId = sanitizeOAuthCredentialInput(out.googleSheetId);
|
|
249
|
+
}
|
|
173
250
|
await writeFile(CONFIG_PATH, JSON.stringify(out, null, 2), 'utf8');
|
|
174
251
|
}
|
|
175
252
|
|
|
@@ -189,11 +266,16 @@ export async function saveConfig(cfg) {
|
|
|
189
266
|
}
|
|
190
267
|
const out = {
|
|
191
268
|
...existing,
|
|
192
|
-
clientId: cfg.clientId,
|
|
193
|
-
clientSecret: cfg.clientSecret,
|
|
269
|
+
clientId: sanitizeOAuthCredentialInput(cfg.clientId),
|
|
270
|
+
clientSecret: sanitizeOAuthCredentialInput(cfg.clientSecret),
|
|
194
271
|
redirectPort: cfg.redirectPort ?? DEFAULT_OAUTH_REDIRECT_PORT,
|
|
195
272
|
};
|
|
196
|
-
if (cfg.googleSheetId !== undefined)
|
|
273
|
+
if (cfg.googleSheetId !== undefined) {
|
|
274
|
+
out.googleSheetId =
|
|
275
|
+
typeof cfg.googleSheetId === 'string'
|
|
276
|
+
? sanitizeOAuthCredentialInput(cfg.googleSheetId)
|
|
277
|
+
: cfg.googleSheetId;
|
|
278
|
+
}
|
|
197
279
|
delete out.sheetTab;
|
|
198
280
|
delete out.sheetDailyPrefix;
|
|
199
281
|
await writeFile(CONFIG_PATH, JSON.stringify(out, null, 2), 'utf8');
|
package/src/oauth.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
loadSheetSyncTarget,
|
|
15
15
|
loadOverlayAcceleratorFromConfigSync,
|
|
16
16
|
loadOverlayPlatformFromConfigSync,
|
|
17
|
+
sanitizeOAuthCredentialInput,
|
|
17
18
|
} from './config.js';
|
|
18
19
|
import { saveTokens, loadTokens } from './tokens.js';
|
|
19
20
|
import { TOKENS_PATH } from './paths.js';
|
|
@@ -77,6 +78,57 @@ function escapeHtml(s) {
|
|
|
77
78
|
.replace(/'/g, ''');
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
/**
|
|
82
|
+
* User-visible explanation when exchanging auth code for tokens fails (e.g. invalid_client).
|
|
83
|
+
* @param {unknown} err
|
|
84
|
+
*/
|
|
85
|
+
function formatOAuthTokenExchangeMessage(err) {
|
|
86
|
+
const g = /** @type {{ response?: { data?: { error?: string; error_description?: string } } } } */ (
|
|
87
|
+
err
|
|
88
|
+
);
|
|
89
|
+
const code = g.response?.data?.error;
|
|
90
|
+
const desc = (g.response?.data?.error_description || '').trim();
|
|
91
|
+
const fallback = /** @type {Error} */ (err)?.message || String(err);
|
|
92
|
+
|
|
93
|
+
if (code === 'invalid_client') {
|
|
94
|
+
const secretInvalid = /client secret is invalid|invalid_client_secret/i.test(desc);
|
|
95
|
+
const secretHint = secretInvalid
|
|
96
|
+
? 'Google says the Client Secret is wrong for this Client ID. Open Cloud Console → APIs & Services → Credentials → your Web client → reset secret or download JSON again; paste client_id and client_secret from that JSON into the CopyHub wizard (Safari/Chrome may autofill an old secret — clear fields first). '
|
|
97
|
+
: '';
|
|
98
|
+
return (
|
|
99
|
+
'OAuth invalid_client: Google rejected the Client ID / Client Secret pair. ' +
|
|
100
|
+
secretHint +
|
|
101
|
+
'Use OAuth client type "Web application" and add redirect URI http://127.0.0.1:<port>/oauth2callback (port matches CopyHub). Prefer pasting values from the client\'s Download JSON file to avoid typos. ' +
|
|
102
|
+
'If COPYHUB_GOOGLE_CLIENT_ID / COPYHUB_GOOGLE_CLIENT_SECRET exist in shell or ~/.copyhub/.env, they must match this client or remove both so ~/.copyhub/config.json is used. ' +
|
|
103
|
+
(desc ? `Google says: ${desc}` : `(${fallback})`)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (code === 'invalid_grant') {
|
|
108
|
+
return (
|
|
109
|
+
'OAuth invalid_grant: the authorization code expired or was already used. Close extra browser tabs and run copyhub login again.' +
|
|
110
|
+
(desc ? ` ${desc}` : '')
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return desc ? `${code || 'OAuth'}: ${desc}` : fallback;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** @param {string} bodyText */
|
|
118
|
+
function oauthTokenExchangeErrorPage(bodyText) {
|
|
119
|
+
const esc = escapeHtml(bodyText);
|
|
120
|
+
return `<!DOCTYPE html>
|
|
121
|
+
<html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>CopyHub OAuth error</title>
|
|
122
|
+
<style>
|
|
123
|
+
body{font-family:system-ui,sans-serif;margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#f0f4fa;padding:24px;}
|
|
124
|
+
.box{background:#fff;padding:28px 32px;border-radius:14px;max-width:520px;box-shadow:0 4px 24px rgba(20,24,36,.08);line-height:1.55;color:#141824;}
|
|
125
|
+
h1{font-size:1.15rem;margin:0 0 12px;}
|
|
126
|
+
pre{white-space:pre-wrap;word-break:break-word;background:#f8fafc;padding:12px 14px;border-radius:8px;font-size:13px;color:#334155;border:1px solid #e2e8f0;}
|
|
127
|
+
code{background:#eff6ff;padding:2px 8px;border-radius:6px;font-size:13px;}
|
|
128
|
+
</style></head><body><div class="box"><h1>Could not finish sign-in</h1><pre>${esc}</pre>
|
|
129
|
+
<p style="margin-top:16px;color:#64748b;font-size:14px;">Fix the issue, then run <code>copyhub login</code> again.</p></div></body></html>`;
|
|
130
|
+
}
|
|
131
|
+
|
|
80
132
|
/**
|
|
81
133
|
* @param {string} bootstrapToken
|
|
82
134
|
* @param {number} listenPort
|
|
@@ -181,7 +233,8 @@ function credentialSetupPageHtml(bootstrapToken, listenPort) {
|
|
|
181
233
|
<div class="wrap">
|
|
182
234
|
<div class="brand">CopyHub</div>
|
|
183
235
|
<h1>Google Cloud OAuth</h1>
|
|
184
|
-
<p class="sub">Paste your OAuth 2.0 Client ID and Client Secret (same as <code>${ENV_GOOGLE_CLIENT_ID}</code> / <code>${ENV_GOOGLE_CLIENT_SECRET}</code> in <code>.env</code>). Stored in <code>~/.copyhub/config.json</code>.</p>
|
|
236
|
+
<p class="sub">Paste your OAuth 2.0 Client ID and Client Secret (same as <code>${ENV_GOOGLE_CLIENT_ID}</code> / <code>${ENV_GOOGLE_CLIENT_SECRET}</code> in <code>.env</code>). Stored in <code>~/.copyhub/config.json</code>. After saving here, CopyHub uses this pair for sign-in — not leftover variables from shell or <code>.env</code>.</p>
|
|
237
|
+
<p class="hint" style="margin-bottom:20px;"><strong>Mac / Safari:</strong> copy from Google Cloud → Credentials → your <strong>Web client</strong> → <strong>Download JSON</strong> and paste <code>client_id</code> / <code>client_secret</code> exactly. Clear both fields if the browser autofills an old secret.</p>
|
|
185
238
|
|
|
186
239
|
<div class="card">
|
|
187
240
|
<p class="hint" style="margin-top:0;"><strong>Authorized redirect URI</strong> in Google Cloud Console must include:</p>
|
|
@@ -189,13 +242,13 @@ function credentialSetupPageHtml(bootstrapToken, listenPort) {
|
|
|
189
242
|
<p class="hint">Port comes from <code>${ENV_OAUTH_REDIRECT_PORT}</code> (currently <strong>${listenPort}</strong>) or your saved config.</p>
|
|
190
243
|
</div>
|
|
191
244
|
|
|
192
|
-
<form method="POST" action="/credentials">
|
|
245
|
+
<form method="POST" action="/credentials" autocomplete="off">
|
|
193
246
|
<input type="hidden" name="t" value="${tVal}" />
|
|
194
247
|
<div class="card">
|
|
195
248
|
<label class="field-label" for="cid">Client ID</label>
|
|
196
|
-
<input id="cid" type="text" name="clientId" autocomplete="off" spellcheck="false" required />
|
|
249
|
+
<input id="cid" type="text" name="clientId" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" required />
|
|
197
250
|
<label class="field-label" for="csec" style="margin-top:16px;">Client secret</label>
|
|
198
|
-
<input id="csec" type="
|
|
251
|
+
<input id="csec" type="text" name="clientSecret" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" required placeholder="Usually starts with GOCSPX-" />
|
|
199
252
|
</div>
|
|
200
253
|
<button type="submit" class="submit">Save and continue to Google sign-in</button>
|
|
201
254
|
</form>
|
|
@@ -270,8 +323,8 @@ async function runCredentialBootstrap() {
|
|
|
270
323
|
res.end('Forbidden');
|
|
271
324
|
return;
|
|
272
325
|
}
|
|
273
|
-
const clientId = (params.get('clientId')
|
|
274
|
-
const clientSecret = (params.get('clientSecret')
|
|
326
|
+
const clientId = sanitizeOAuthCredentialInput(params.get('clientId'));
|
|
327
|
+
const clientSecret = sanitizeOAuthCredentialInput(params.get('clientSecret'));
|
|
275
328
|
if (!clientId || !clientSecret) {
|
|
276
329
|
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
277
330
|
res.end('<p>Client ID and Client secret are required.</p>');
|
|
@@ -741,7 +794,18 @@ export async function runLoginFlow() {
|
|
|
741
794
|
server.close(() => reject(new Error('Missing authorization code')));
|
|
742
795
|
return;
|
|
743
796
|
}
|
|
744
|
-
|
|
797
|
+
let tokens;
|
|
798
|
+
try {
|
|
799
|
+
const exchanged = await oauth2Client.getToken(code);
|
|
800
|
+
tokens = exchanged.tokens;
|
|
801
|
+
} catch (tokenErr) {
|
|
802
|
+
const msg = formatOAuthTokenExchangeMessage(tokenErr);
|
|
803
|
+
console.error('[CopyHub OAuth]', msg);
|
|
804
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
805
|
+
res.end(oauthTokenExchangeErrorPage(msg));
|
|
806
|
+
server.close(() => reject(new Error(msg)));
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
745
809
|
oauth2Client.setCredentials(tokens);
|
|
746
810
|
await saveTokens(tokens);
|
|
747
811
|
setupToken = randomBytes(24).toString('hex');
|