Skip to content

Commit be62ae1

Browse files
authored
Merge pull request #51 from Waynting/fix/credential-handling
Credential handling fixes from #50: logout clears .olauth, the password is no longer persisted by default, and olcli auth prompts instead of taking --password on the command line. Author: @Waynting. Verified before merging: npm ci, lint and build clean, 58 tests passing against 38 on main, CI green on Node 20.18.1 and 24. The PR sits on the current main with no drift.
2 parents d0e1fba + b577265 commit be62ae1

8 files changed

Lines changed: 606 additions & 16 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [Unreleased]
6+
7+
### Fixed
8+
- **`olcli logout` left `.olauth` behind and reported success anyway** ([#50](https://github.com/aloth/olcli/issues/50)) - it cleared the global config and printed `Credentials cleared`, while the `.olauth` file in the current directory survived. That file is consulted *ahead* of the global config, so the user stayed authenticated in that directory - and in `olcli-mcp`, which reads it too. `logout` now clears both and lists what it actually removed
9+
- Environment variables cannot be unset by a child process, so `OVERLEAF_SESSION` and `OVERLEAF_EMAIL`/`OVERLEAF_PASSWORD` are now reported instead of ignored. They outrank everything on disk, and a logout that stays silent about them repeats the original mistake in a different place
10+
- **`olcli auth` claimed `Password login saved.` even under `--no-save-password`** - the same class of bug: a message stating an outcome that did not happen. It now reports what was actually stored
11+
12+
### Changed
13+
- **The account password is no longer persisted by default** ([#50](https://github.com/aloth/olcli/issues/50)) - it is written only when you ask for it with `--save-password`. The session cookie is stored either way and is what every later command uses; the password only bought an automatic re-login after that cookie expired. A cookie is scoped to olcli and rotates, a password is reusable everywhere and cannot be revoked without changing it
14+
- `--no-save-password` still parses and still means "do not save", so existing scripts keep working - it is simply the default now
15+
- **Behaviour change for self-hosted users:** an expired session no longer re-logs in silently. Re-run `olcli auth`, pass `--save-password` to keep the old behaviour, or set `OVERLEAF_EMAIL`/`OVERLEAF_PASSWORD`
16+
- **`olcli auth --password` is now optional and prompts instead** ([#50](https://github.com/aloth/olcli/issues/50)) - passing it puts the password in shell history, so `olcli auth --email you@example.com` now reads it from the terminal without echoing. The flag still works and warns; with no terminal available, the error names `OVERLEAF_EMAIL`/`OVERLEAF_PASSWORD`, which every command already reads
17+
- Keystroke handling is a pure reducer so it can be tested without a pty. Driving the real prompt over one is what surfaced the bug it now guards: filtering only the ESC of an arrow key left the printable `[` and `A` behind and silently appended them to the password
18+
- **`olcli check` now reports whether a password is stored** and whether a `.olauth` file is present, never the values. Answering "is my password on disk?" previously meant opening the config file
19+
520
## [0.11.0] - 2026-09-04
621

722
### Added

‎README.md‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,16 @@ olcli auth --cookie "your_session_cookie_value"
8383
**Email/password** (self-hosted without reCAPTCHA):
8484

8585
```bash
86-
olcli auth --email "you@example.com" --password "your_password"
86+
olcli auth --email "you@example.com"
87+
# prompts for the password, so it stays out of your shell history
8788
```
8889

90+
The password is **not stored** unless you pass `--save-password`. A session
91+
cookie is saved either way and is what later commands use; the password only
92+
buys an automatic re-login once that cookie expires. For scripts, set
93+
`OVERLEAF_EMAIL` and `OVERLEAF_PASSWORD` — every command reads them, so a
94+
scripted run never needs `olcli auth` at all.
95+
8996
### 2. List Projects
9097

9198
```bash
@@ -128,7 +135,7 @@ All commands auto-detect the project when run from a synced directory (contains
128135
|---------|-------------|
129136
| `olcli auth` | Set session cookie or login with email/password |
130137
| `olcli whoami` | Check authentication status |
131-
| `olcli logout` | Clear stored credentials |
138+
| `olcli logout` | Clear the global config and the local `.olauth`, reporting each |
132139
| `olcli list` | List all projects |
133140
| `olcli info [project]` | Show project details and file list |
134141
| `olcli pull [project] [dir]` | Download project files to local directory |
@@ -284,6 +291,23 @@ hardcoded here: on macOS it lands under `~/Library/Preferences/`, on Linux under
284291
which is usually your LaTeX project. Add it to that project's `.gitignore`
285292
before committing.
286293

294+
### What is stored, and how to clear it
295+
296+
Everything is stored in plaintext, so it is worth knowing what is on disk:
297+
298+
| Credential | Stored by default | Where |
299+
|---|---|---|
300+
| Session cookie | yes | global config, or `.olauth` with `--save-local` |
301+
| Email + password | **no** — only with `--save-password` | global config |
302+
303+
`olcli check` reports what exists without printing any secret.
304+
305+
`olcli logout` clears the global config **and** the `.olauth` file in the
306+
current directory, then lists what it removed. It cannot unset environment
307+
variables, so if `OVERLEAF_SESSION` or `OVERLEAF_EMAIL`/`OVERLEAF_PASSWORD` are
308+
set, it says so instead of implying you are logged out — those take precedence
309+
over anything on disk.
310+
287311
### Self-hosted Overleaf
288312

289313
```bash

‎SKILL.md‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ Clear stored credentials:
6060
olcli logout
6161
```
6262

63+
Clears the global config and the `.olauth` file in the current directory, and
64+
reports each. Environment variables cannot be unset by a child process, so
65+
`OVERLEAF_SESSION` and `OVERLEAF_EMAIL`/`OVERLEAF_PASSWORD` are reported rather
66+
than silently ignored — they outrank anything on disk.
67+
68+
For unattended use, prefer `OVERLEAF_EMAIL`/`OVERLEAF_PASSWORD` over
69+
`olcli auth --password`: every command reads them, and nothing is written to
70+
disk or to shell history.
71+
6372
### Self-hosted Overleaf
6473

6574
```bash
@@ -255,9 +264,9 @@ zip arxiv.zip *.tex main.bbl figures/*.pdf
255264
| Command | Description |
256265
|---------|-------------|
257266
| `olcli auth --cookie <value>` | Authenticate with session cookie |
258-
| `olcli auth --email <e> --password <p>` | Authenticate with password (self-hosted) |
267+
| `olcli auth --email <e>` | Authenticate with password, prompted (self-hosted) |
259268
| `olcli whoami` | Check authentication status |
260-
| `olcli logout` | Clear stored credentials |
269+
| `olcli logout` | Clear the global config and the local `.olauth` |
261270
| `olcli check` | Show config paths and credential sources |
262271
| `olcli list` | List all projects |
263272
| `olcli project create <name>` | Create a blank or example project |

‎src/cli.ts‎

Lines changed: 116 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ import {
3838
setTimeout,
3939
getPasswordCredentials,
4040
setPasswordCredentials,
41+
clearOlAuth,
42+
inspectStoredCredentials,
4143
type PasswordCredentials
4244
} from './config.js';
45+
import { promptHidden, PromptCancelled, NotATerminal } from './prompt.js';
4346

4447
const program = new Command();
4548

@@ -158,9 +161,18 @@ program
158161
.description('Authenticate with Overleaf using a session cookie or email/password')
159162
.option('--cookie <session>', 'Session cookie (overleaf_session2 value)')
160163
.option('--email <email>', 'Account email for password login')
161-
.option('--password <password>', 'Account password for password login')
162-
.option('--no-save-password', 'Do not persist email/password credentials')
164+
.option('--password <password>', 'Account password (omit to be prompted; see warning below)')
165+
.option('--save-password', 'Persist the password in the config file, in plaintext')
166+
.option('--no-save-password', 'Do not persist the password (the default; kept for existing scripts)')
163167
.option('--save-local', 'Save to .olauth in current directory')
168+
.addHelpText('after', `
169+
The password is not stored unless you ask for it with --save-password. A
170+
session cookie is stored either way, and that is what later commands use; the
171+
password only buys an automatic re-login once the cookie expires.
172+
173+
Passing --password puts the password in your shell history. Omit it to be
174+
prompted instead, or set OVERLEAF_EMAIL and OVERLEAF_PASSWORD, which every
175+
command reads without needing 'auth' at all.`)
164176
.action(async (options) => {
165177
if (!options.cookie && !options.email && !options.password) {
166178
console.log(chalk.yellow('To authenticate, provide a session cookie:'));
@@ -173,7 +185,8 @@ program
173185
console.log(chalk.cyan(' olcli auth --cookie "your_session_cookie_value"'));
174186
console.log();
175187
console.log('Or log in with email/password:');
176-
console.log(chalk.cyan(' olcli auth --email "you@example.com" --password "your_password"'));
188+
console.log(chalk.cyan(' olcli auth --email "you@example.com"'));
189+
console.log(chalk.dim(' (prompts for the password, so it stays out of your shell history)'));
177190
console.log();
178191
console.log('Or set OVERLEAF_SESSION environment variable');
179192
return;
@@ -184,11 +197,41 @@ program
184197
process.exit(1);
185198
}
186199

187-
if (!options.cookie && (!options.email || !options.password)) {
188-
console.error(chalk.red('Both --email and --password are required for password login.'));
200+
if (!options.cookie && !options.email) {
201+
console.error(chalk.red('--email is required for password login.'));
189202
process.exit(1);
190203
}
191204

205+
// Resolve the password before the spinner starts: a prompt and a spinner
206+
// both own the terminal, and ora would redraw over the prompt line.
207+
let password: string | undefined = options.password;
208+
if (!options.cookie) {
209+
if (password) {
210+
console.log(chalk.yellow('⚠ --password is now in your shell history.'));
211+
console.log(chalk.dim(' Omit it to be prompted, or set OVERLEAF_EMAIL/OVERLEAF_PASSWORD.'));
212+
} else {
213+
try {
214+
password = await promptHidden(`Password for ${options.email}: `);
215+
} catch (error: any) {
216+
if (error instanceof NotATerminal) {
217+
console.error(chalk.red('No terminal available to prompt for a password.'));
218+
console.error('Set OVERLEAF_EMAIL and OVERLEAF_PASSWORD instead — every command reads them,');
219+
console.error("so a scripted run does not need 'olcli auth' at all.");
220+
process.exit(1);
221+
}
222+
if (error instanceof PromptCancelled) {
223+
console.error(chalk.red('Cancelled.'));
224+
process.exit(1);
225+
}
226+
throw error;
227+
}
228+
if (!password) {
229+
console.error(chalk.red('Password must not be empty.'));
230+
process.exit(1);
231+
}
232+
}
233+
}
234+
192235
const spinner = ora('Verifying session...').start();
193236
try {
194237
const baseUrl = (program.opts().baseUrl as string | undefined) || getBaseUrl();
@@ -208,15 +251,30 @@ program
208251
}
209252
} else {
210253
spinner.text = 'Logging in with email/password...';
211-
const client = await OverleafClient.fromPasswordLogin(options.email, options.password, baseUrl);
254+
const client = await OverleafClient.fromPasswordLogin(options.email, password!, baseUrl);
212255
const projects = await client.listProjects();
213256
persistClientSession(client, cookieName);
214257
setBaseUrl(baseUrl);
215-
if (options.savePassword !== false) {
216-
setPasswordCredentials(options.email, options.password);
258+
259+
// Opt-in, not opt-out. The session cookie persisted just above is what
260+
// later commands actually use; the password only buys an automatic
261+
// re-login after that cookie expires, and it is stored in plaintext.
262+
// A cookie is scoped to olcli and rotates; a password is reusable
263+
// everywhere and cannot be revoked without changing it. See issue #50.
264+
const savePassword = options.savePassword === true;
265+
if (savePassword) {
266+
setPasswordCredentials(options.email, password!);
217267
}
218268

219-
spinner.succeed(`Authenticated! Found ${projects.length} projects. Password login saved.`);
269+
// The old message said "Password login saved." unconditionally - even
270+
// under --no-save-password, which had just prevented exactly that.
271+
spinner.succeed(`Authenticated! Found ${projects.length} projects.`);
272+
if (savePassword) {
273+
console.log(chalk.yellow('Password stored in plaintext in the config file.'));
274+
} else {
275+
console.log(chalk.dim('Session cookie stored. The password was not saved; re-run'));
276+
console.log(chalk.dim('olcli auth when the session expires, or use --save-password.'));
277+
}
220278
}
221279

222280
console.log(chalk.dim(`Config saved to: ${getConfigPath()}`));
@@ -251,9 +309,41 @@ program
251309
program
252310
.command('logout')
253311
.description('Clear stored credentials')
312+
.addHelpText('after', `
313+
Clears the global config and the .olauth file in the current directory, and
314+
reports each one separately. Environment variables cannot be cleared by a
315+
child process, so OVERLEAF_SESSION and OVERLEAF_EMAIL/OVERLEAF_PASSWORD are
316+
reported instead of silently ignored - both take precedence over anything on
317+
disk.`)
254318
.action(() => {
319+
// Read before clearing: afterwards there is nothing left to report on.
320+
const before = inspectStoredCredentials();
321+
255322
clearConfig();
256-
console.log(chalk.green('Credentials cleared'));
323+
const removedOlAuth = clearOlAuth();
324+
325+
const cleared: string[] = [];
326+
if (before.sessionCookie) cleared.push('session cookie (global config)');
327+
if (before.password) cleared.push('saved password (global config)');
328+
if (removedOlAuth) cleared.push(removedOlAuth);
329+
330+
if (cleared.length === 0) {
331+
console.log('Nothing stored to clear.');
332+
} else {
333+
console.log(chalk.green('Cleared:'));
334+
for (const item of cleared) console.log(` ${item}`);
335+
}
336+
337+
// The reason this command was wrong before: it announced success while a
338+
// higher-precedence source kept the user authenticated. Anything olcli
339+
// cannot clear has to be said out loud, or the message is a lie again.
340+
if (before.envSession || before.envPassword) {
341+
console.log();
342+
console.log(chalk.yellow('Still authenticated in this shell:'));
343+
if (before.envSession) console.log(' OVERLEAF_SESSION is set');
344+
if (before.envPassword) console.log(' OVERLEAF_EMAIL and OVERLEAF_PASSWORD are set');
345+
console.log(chalk.dim(' These outrank anything on disk. Unset them to finish logging out.'));
346+
}
257347
});
258348

259349
// ─────────────────────────────────────────────────────────────────────────────
@@ -1961,6 +2051,22 @@ program
19612051
} else {
19622052
console.log(chalk.yellow('✗ No session cookie found'));
19632053
}
2054+
2055+
// A stored password is a credential source this command used to omit,
2056+
// which made it impossible to answer "is my password on disk?" without
2057+
// opening the config file. Never print the value - only whether it exists
2058+
// and which source it came from.
2059+
const stored = inspectStoredCredentials();
2060+
if (stored.envPassword) {
2061+
console.log(chalk.yellow('⚠ Password set via OVERLEAF_EMAIL/OVERLEAF_PASSWORD'));
2062+
} else if (stored.password) {
2063+
console.log(chalk.yellow('⚠ Password stored in plaintext in the config file'));
2064+
console.log(chalk.dim(" Remove it with 'olcli logout', then re-auth without --save-password."));
2065+
}
2066+
2067+
if (stored.olAuthPath) {
2068+
console.log(chalk.dim(` .olauth present: ${stored.olAuthPath}`));
2069+
}
19642070
});
19652071

19662072
program.parse(process.argv);

‎src/config.ts‎

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import Conf from 'conf';
6-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
6+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
77
import { join } from 'node:path';
88

99
interface OlcliConfig {
@@ -135,10 +135,67 @@ export function getConfigPath(): string {
135135
return config.path;
136136
}
137137

138+
/**
139+
* Where a `.olauth` file would live for a given directory.
140+
*
141+
* Defaults to the current working directory, which is what `saveOlAuth` and
142+
* `getSessionCookie` both use.
143+
*/
144+
export function getOlAuthPath(dir?: string): string {
145+
return join(dir || process.cwd(), '.olauth');
146+
}
147+
138148
/**
139149
* Save session cookie in .olauth format for compatibility
140150
*/
141151
export function saveOlAuth(cookie: string, path?: string): void {
142-
const authPath = path || join(process.cwd(), '.olauth');
152+
const authPath = path || getOlAuthPath();
143153
writeFileSync(authPath, `${getSessionCookieName()}=${cookie}`, 'utf-8');
144154
}
155+
156+
/**
157+
* Delete the `.olauth` file for a directory, if there is one.
158+
*
159+
* Returns the path that was removed, or null when there was nothing to
160+
* remove. `logout` needs the distinction to report what it actually did.
161+
*/
162+
export function clearOlAuth(dir?: string): string | null {
163+
const authPath = getOlAuthPath(dir);
164+
if (!existsSync(authPath)) return null;
165+
rmSync(authPath);
166+
return authPath;
167+
}
168+
169+
/**
170+
* What credentials exist right now, and where.
171+
*
172+
* Deliberately reports every source `getSessionCookie` and
173+
* `getPasswordCredentials` consult, including the two that no command can
174+
* clear. `logout` used to clear the global config and announce success while
175+
* a `.olauth` file - which takes precedence over it - stayed on disk and kept
176+
* the user authenticated. Reporting per source is what stops that message
177+
* from being wrong again. See issue #50.
178+
*/
179+
export interface StoredCredentials {
180+
/** Session cookie in the global config file. */
181+
sessionCookie: boolean;
182+
/** Email/password pair in the global config file. */
183+
password: boolean;
184+
/** Path of the `.olauth` file, when one exists. Takes precedence over the config. */
185+
olAuthPath: string | null;
186+
/** `OVERLEAF_SESSION` is set. Takes precedence over everything, and logout cannot unset it. */
187+
envSession: boolean;
188+
/** `OVERLEAF_EMAIL`/`OVERLEAF_PASSWORD` are set. Same caveat. */
189+
envPassword: boolean;
190+
}
191+
192+
export function inspectStoredCredentials(dir?: string): StoredCredentials {
193+
const authPath = getOlAuthPath(dir);
194+
return {
195+
sessionCookie: Boolean(config.get('sessionCookie')),
196+
password: Boolean(config.get('loginEmail') || config.get('loginPassword')),
197+
olAuthPath: existsSync(authPath) ? authPath : null,
198+
envSession: Boolean(process.env.OVERLEAF_SESSION),
199+
envPassword: Boolean(process.env.OVERLEAF_EMAIL && process.env.OVERLEAF_PASSWORD)
200+
};
201+
}

0 commit comments

Comments
 (0)