Skip to content

Commit 36d4e64

Browse files
committed
fix(auth): refresh token before token view
1 parent 54fbf7c commit 36d4e64

2 files changed

Lines changed: 78 additions & 7 deletions

File tree

src/commands/auth/auth.test.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,16 +284,54 @@ describe('auth command', () => {
284284
vi.unstubAllEnvs()
285285
})
286286

287-
it('prints exactly the stored token to stdout with no envelope (pipe-safe)', async () => {
287+
it('prints exactly the current token snapshot to stdout with no envelope (pipe-safe)', async () => {
288288
vi.stubEnv(TOKEN_ENV_VAR, '')
289289
storeMocks.active.mockResolvedValue(STORED_SNAPSHOT)
290+
mockGetApiTokenSnapshot.mockResolvedValue(STORED_SNAPSHOT)
290291

291292
await createProgram().parseAsync(['node', 'tdc', 'auth', 'token', 'view'])
292293

294+
expect(mockGetApiTokenSnapshot).toHaveBeenCalledWith(undefined)
293295
expect(stdoutPayload()).toBe('tk_stored_1234567890')
294296
expect(consoleSpy).not.toHaveBeenCalled()
295297
})
296298

299+
it('refreshes an expired OAuth token before printing', async () => {
300+
vi.stubEnv(TOKEN_ENV_VAR, '')
301+
const oauthAccount: CommsAccount = {
302+
...STORED_ACCOUNT,
303+
oauthClientId: 'tdd_123',
304+
authBaseUrl: 'https://todoist.com',
305+
authResource: 'https://comms.todoist.com',
306+
}
307+
storeMocks.active.mockResolvedValue({
308+
token: 'tk_expired_1234567890',
309+
account: oauthAccount,
310+
})
311+
mockGetApiTokenSnapshot.mockResolvedValue({
312+
token: 'tk_refreshed_1234567890',
313+
account: oauthAccount,
314+
})
315+
316+
await createProgram().parseAsync(['node', 'tdc', 'auth', 'token', 'view'])
317+
318+
expect(mockGetApiTokenSnapshot).toHaveBeenCalledWith(undefined)
319+
expect(stdoutPayload()).toBe('tk_refreshed_1234567890')
320+
})
321+
322+
it('prints manual tokens without using the OAuth refresh snapshot path', async () => {
323+
vi.stubEnv(TOKEN_ENV_VAR, '')
324+
storeMocks.active.mockResolvedValue({
325+
token: 'manual_token_1234567890',
326+
account: { id: '', label: '', authMode: 'unknown', authScope: '' },
327+
})
328+
329+
await createProgram().parseAsync(['node', 'tdc', 'auth', 'token', 'view'])
330+
331+
expect(mockGetApiTokenSnapshot).not.toHaveBeenCalled()
332+
expect(stdoutPayload()).toBe('manual_token_1234567890')
333+
})
334+
297335
it('refuses to print when the env var is set so the CLI does not disclose an unmanaged token', async () => {
298336
vi.stubEnv(TOKEN_ENV_VAR, 'env_token_supplied_externally')
299337

@@ -319,6 +357,7 @@ describe('auth command', () => {
319357
it('matches per-command --user against the stored account by id', async () => {
320358
vi.stubEnv(TOKEN_ENV_VAR, '')
321359
storeMocks.active.mockResolvedValue(STORED_SNAPSHOT)
360+
mockGetApiTokenSnapshot.mockResolvedValue(STORED_SNAPSHOT)
322361

323362
await createProgram().parseAsync([
324363
'node',
@@ -331,6 +370,7 @@ describe('auth command', () => {
331370
])
332371

333372
expect(storeMocks.active).toHaveBeenCalledWith('1')
373+
expect(mockGetApiTokenSnapshot).toHaveBeenCalledWith('1')
334374
expect(stdoutPayload()).toBe('tk_stored_1234567890')
335375
})
336376

@@ -373,16 +413,18 @@ describe('auth command', () => {
373413
vi.unstubAllEnvs()
374414
})
375415

376-
it('threads `tdc --user <ref> auth token view` into store.active', async () => {
416+
it('threads `tdc --user <ref> auth token view` into store.active and token refresh', async () => {
377417
vi.stubEnv(TOKEN_ENV_VAR, '')
378418
storeMocks.list.mockResolvedValue(STORED_RECORDS)
379419
storeMocks.active.mockResolvedValue(STORED_SNAPSHOT)
420+
mockGetApiTokenSnapshot.mockResolvedValue(STORED_SNAPSHOT)
380421
process.argv = ['node', 'tdc', '--user', '1', 'auth', 'token', 'view']
381422
resetGlobalArgs()
382423

383424
await createProgram().parseAsync(['node', 'tdc', 'auth', 'token', 'view'])
384425

385426
expect(storeMocks.active).toHaveBeenCalledWith('1')
427+
expect(mockGetApiTokenSnapshot).toHaveBeenCalledWith('1')
386428
expect(writeSpy.mock.calls.map((c: unknown[]) => String(c[0])).join('')).toBe(
387429
'tk_stored_1234567890',
388430
)

src/commands/auth/index.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,48 @@
1-
import { attachTokenViewCommand } from '@doist/cli-core/auth'
1+
import { type AccountRef, attachTokenViewCommand } from '@doist/cli-core/auth'
22
import { Command } from 'commander'
3-
import { createCommsTokenStore } from '../../lib/auth-provider.js'
4-
import { TOKEN_ENV_VAR } from '../../lib/auth.js'
3+
import {
4+
createCommsTokenStore,
5+
isManualTokenAccount,
6+
type CommsTokenStore,
7+
} from '../../lib/auth-provider.js'
8+
import { getApiTokenSnapshot, NoTokenError, TOKEN_ENV_VAR } from '../../lib/auth.js'
59
import { getRequestedUserRef } from '../../lib/global-args.js'
610
import { attachCommsLoginCommand } from './login.js'
711
import { attachCommsLogoutCommand } from './logout.js'
812
import { attachCommsStatusCommand } from './status.js'
913
import { withUserRefAware } from './store-wrap.js'
1014
import { loginWithToken } from './token.js'
1115

16+
function withRefreshAwareTokenViewStore(
17+
store: CommsTokenStore,
18+
requestedRef: AccountRef | undefined,
19+
): CommsTokenStore {
20+
// cli-core's token-view command reads `store.active()` directly. Wrap that
21+
// read so OAuth accounts go through the same refresh-capable snapshot path
22+
// as `auth status`, while preserving token-view's existing miss/env/manual
23+
// token behavior.
24+
return Object.assign(Object.create(store) as CommsTokenStore, {
25+
active: async (ref?: AccountRef) => {
26+
const effectiveRef = ref ?? requestedRef
27+
const snapshot = await store.active(effectiveRef)
28+
if (!snapshot || isManualTokenAccount(snapshot.account)) return snapshot
29+
30+
try {
31+
return await getApiTokenSnapshot(effectiveRef)
32+
} catch (error) {
33+
if (error instanceof NoTokenError) return null
34+
throw error
35+
}
36+
},
37+
})
38+
}
39+
1240
export function registerAuthCommand(program: Command): void {
1341
const auth = program.command('auth').description('Manage authentication')
1442

1543
const store = createCommsTokenStore()
16-
const refAware = withUserRefAware(store, getRequestedUserRef())
44+
const requestedRef = getRequestedUserRef()
45+
const refAware = withUserRefAware(store, requestedRef)
1746

1847
attachCommsLoginCommand(auth, store)
1948
attachCommsLogoutCommand(auth, refAware)
@@ -30,7 +59,7 @@ export function registerAuthCommand(program: Command): void {
3059

3160
attachTokenViewCommand(tokenCmd, {
3261
name: 'view',
33-
store: refAware,
62+
store: withRefreshAwareTokenViewStore(store, requestedRef),
3463
envVarName: TOKEN_ENV_VAR,
3564
description:
3665
'Print the stored API token for the active user (or --user <ref>) to stdout for use in scripts',

0 commit comments

Comments
 (0)