Skip to content

Commit 4a0c7fb

Browse files
committed
Use XDG config dir for OAuth credentials
1 parent 884bf93 commit 4a0c7fb

3 files changed

Lines changed: 31 additions & 9 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ The server authenticates to the Nutrient DWS API (`https://api.nutrient.io`) usi
236236
| **API key** | `NUTRIENT_DWS_API_KEY` is set | Static key passed as Bearer token to DWS API |
237237
| **OAuth browser flow** | No API key set | Opens browser for Nutrient OAuth consent, caches token locally |
238238

239-
When no API key is configured, the server opens a browser-based OAuth flow on the first tool call (similar to `gh auth login`). Tokens are cached at `~/.nutrient/credentials.json` and refreshed automatically.
239+
When no API key is configured, the server opens a browser-based OAuth flow on the first tool call (similar to `gh auth login`). Tokens are cached at `$XDG_CONFIG_HOME/nutrient/credentials.json` or `~/.config/nutrient/credentials.json` and refreshed automatically.
240240

241241
### Environment Variables
242242

@@ -253,7 +253,7 @@ When no API key is configured, the server opens a browser-based OAuth flow on th
253253

254254
### Security Note: Token Storage
255255

256-
When using the OAuth browser flow, access tokens and refresh tokens are cached in plaintext at `~/.nutrient/credentials.json` (permissions `0600`). This file contains credentials equivalent to your API key. Do not commit it to version control or include it in shared backups.
256+
When using the OAuth browser flow, access tokens and refresh tokens are cached in plaintext at `$XDG_CONFIG_HOME/nutrient/credentials.json` or `~/.config/nutrient/credentials.json` (permissions `0600`). This file contains credentials equivalent to your API key. Do not commit it to version control or include it in shared backups.
257257

258258
## Troubleshooting
259259

@@ -262,7 +262,7 @@ When using the OAuth browser flow, access tokens and refresh tokens are cached i
262262
If OAuth authentication stops working, delete the cached token file to start fresh:
263263

264264
```bash
265-
rm ~/.nutrient/credentials.json
265+
rm "${XDG_CONFIG_HOME:-$HOME/.config}/nutrient/credentials.json"
266266
```
267267

268268
The server will automatically register a new client and open the browser for consent on the next tool call.
@@ -282,7 +282,7 @@ The server will automatically register a new client and open the browser for con
282282

283283
**"Token exchange failed" or "OAuth authorization failed"?**
284284

285-
- Delete `~/.nutrient/credentials.json` and try again.
285+
- Delete `${XDG_CONFIG_HOME:-$HOME/.config}/nutrient/credentials.json` and try again.
286286
- If using a custom `AUTH_SERVER_URL`, verify the server is reachable and its `/oauth/token` endpoint is working.
287287

288288
**"Dynamic client registration failed"?**
@@ -297,7 +297,7 @@ The server will automatically register a new client and open the browser for con
297297

298298
**Token expired but refresh fails?**
299299

300-
- The server automatically refreshes expired tokens using the cached refresh token. If refresh fails (e.g., the refresh token was revoked), delete `~/.nutrient/credentials.json` — the server will re-authenticate via the browser on the next call.
300+
- The server automatically refreshes expired tokens using the cached refresh token. If refresh fails (e.g., the refresh token was revoked), delete `${XDG_CONFIG_HOME:-$HOME/.config}/nutrient/credentials.json` — the server will re-authenticate via the browser on the next call.
301301

302302
**Files not found?**
303303

src/auth/nutrient-oauth.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export type NutrientOAuthConfig = {
2323
clientName?: string
2424
/** OAuth scopes to request. */
2525
scopes: string[]
26-
/** Path to cache credentials. Defaults to `~/.nutrient/credentials.json`. */
26+
/** Path to cache credentials. Defaults to `$XDG_CONFIG_HOME/nutrient/credentials.json` or `~/.config/nutrient/credentials.json`. */
2727
credentialsPath?: string
2828
/** OAuth resource parameter (RFC 8707). Identifies the target API. */
2929
resource?: string
@@ -38,10 +38,17 @@ const CachedCredentialsSchema = z.object({
3838

3939
type CachedCredentials = z.infer<typeof CachedCredentialsSchema>
4040

41-
const DEFAULT_CREDENTIALS_PATH = join(homedir(), '.nutrient', 'credentials.json')
4241
const FETCH_TIMEOUT_MS = 15_000
4342
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000
4443

44+
export function getDefaultCredentialsPath(
45+
env: NodeJS.ProcessEnv = process.env,
46+
homeDirectory: string = homedir(),
47+
): string {
48+
const configHome = env.XDG_CONFIG_HOME || join(homeDirectory, '.config')
49+
return join(configHome, 'nutrient', 'credentials.json')
50+
}
51+
4552
export function generateCodeVerifier(): string {
4653
return randomBytes(32).toString('base64url')
4754
}
@@ -340,7 +347,7 @@ async function performBrowserOAuthFlow(config: NutrientOAuthConfig): Promise<Cac
340347
* is forced to refresh or re-authenticate.
341348
*/
342349
export async function invalidateCachedToken(config: NutrientOAuthConfig): Promise<void> {
343-
const credentialsPath = config.credentialsPath ?? DEFAULT_CREDENTIALS_PATH
350+
const credentialsPath = config.credentialsPath ?? getDefaultCredentialsPath()
344351
try {
345352
await unlink(credentialsPath)
346353
logger.info('Invalidated cached token', { credentialsPath })
@@ -358,7 +365,7 @@ export async function invalidateCachedToken(config: NutrientOAuthConfig): Promis
358365
* and falls back to a browser-based OAuth flow if no valid token is available.
359366
*/
360367
export async function getToken(config: NutrientOAuthConfig): Promise<string> {
361-
const credentialsPath = config.credentialsPath ?? DEFAULT_CREDENTIALS_PATH
368+
const credentialsPath = config.credentialsPath ?? getDefaultCredentialsPath()
362369

363370
logger.debug('getToken called', { credentialsPath })
364371

tests/nutrient-oauth.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { createServer, type Server } from 'node:http'
77
import {
88
generateCodeVerifier,
99
generateCodeChallenge,
10+
getDefaultCredentialsPath,
1011
isTokenExpired,
1112
readCachedCredentials,
1213
} from '../src/auth/nutrient-oauth.js'
@@ -114,6 +115,20 @@ describe('readCachedCredentials', () => {
114115
})
115116
})
116117

118+
describe('getDefaultCredentialsPath', () => {
119+
it('uses XDG_CONFIG_HOME when set', () => {
120+
const path = getDefaultCredentialsPath({ XDG_CONFIG_HOME: '/tmp/xdg-config' }, '/home/tester')
121+
122+
expect(path).toBe('/tmp/xdg-config/nutrient/credentials.json')
123+
})
124+
125+
it('falls back to ~/.config when XDG_CONFIG_HOME is not set', () => {
126+
const path = getDefaultCredentialsPath({}, '/home/tester')
127+
128+
expect(path).toBe('/home/tester/.config/nutrient/credentials.json')
129+
})
130+
})
131+
117132
// ---------------------------------------------------------------------------
118133
// getToken integration helpers
119134
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)