Skip to content

Commit 3c95642

Browse files
authored
fix: skip device auth when unclaimed environment is active (#115)
* fix: skip device auth when unclaimed environment is active checkStoredAuth only checked for OAuth tokens, so the state machine forced device auth even when a valid unclaimed environment (with apiKey + claimToken) was already active. This caused the CLI to hang on "Waiting for authentication..." after the browser showed success. * test: verify unclaimed env skips device auth in state machine
1 parent 43105c1 commit 3c95642

2 files changed

Lines changed: 57 additions & 1 deletion

File tree

src/lib/installer-core.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import type {
1010
InstallerMachineContext,
1111
BranchCheckOutput,
1212
} from './installer-core.types.js';
13+
import type { EnvFileInfo } from './credential-discovery.js';
14+
import type { StagingCredentials } from './staging-api.js';
1315

1416
// Shared mock actors for reuse across tests
1517
const baseMockActors = {
@@ -378,5 +380,54 @@ describe('InstallerCore State Machine', () => {
378380
expect(actor.getSnapshot().value).toBe('complete');
379381
actor.stop();
380382
});
383+
384+
it('skips device auth when checkStoredAuth returns true (unclaimed env)', async () => {
385+
const emitter = createInstallerEventEmitter();
386+
const options: InstallerOptions = {
387+
debug: false,
388+
forceInstall: false,
389+
installDir: '/test/project',
390+
default: false,
391+
local: true,
392+
ci: false,
393+
skipAuth: true,
394+
dashboard: false,
395+
emitter,
396+
// No CLI credentials — forces credential gathering flow
397+
};
398+
399+
let deviceAuthStarted = false;
400+
401+
const machine = installerMachine.provide({
402+
actors: {
403+
...baseMockActors,
404+
detectEnvFiles: fromPromise<EnvFileInfo, { installDir: string }>(async () => ({
405+
found: false,
406+
})),
407+
checkStoredAuth: fromPromise<boolean, void>(async () => true),
408+
runDeviceAuth: fromPromise(async () => {
409+
deviceAuthStarted = true;
410+
throw new Error('device auth should not be called');
411+
}),
412+
fetchStagingCredentials: fromPromise<StagingCredentials, void>(async () => ({
413+
clientId: 'client_unclaimed',
414+
apiKey: 'sk_test_unclaimed',
415+
})),
416+
},
417+
});
418+
419+
const actor = createActor(machine, {
420+
input: { emitter, options },
421+
});
422+
423+
actor.start();
424+
actor.send({ type: 'START' });
425+
426+
await new Promise((r) => setTimeout(r, 200));
427+
428+
expect(deviceAuthStarted).toBe(false);
429+
expect(actor.getSnapshot().value).toBe('complete');
430+
actor.stop();
431+
});
381432
});
382433
});

src/lib/run-with-core.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
getStagingCredentials,
2828
saveStagingCredentials,
2929
} from './credentials.js';
30-
import { getConfig, saveConfig, getActiveEnvironment } from './config-store.js';
30+
import { getConfig, saveConfig, getActiveEnvironment, isUnclaimedEnvironment } from './config-store.js';
3131
import { checkForEnvFiles, discoverCredentials } from './credential-discovery.js';
3232
import { requestDeviceCode, pollForToken } from './device-auth.js';
3333
import { fetchStagingCredentials as fetchStagingCredentialsApi } from './staging-api.js';
@@ -321,6 +321,11 @@ export async function runWithCore(options: InstallerOptions): Promise<void> {
321321
}),
322322

323323
checkStoredAuth: fromPromise(async () => {
324+
const activeEnv = getActiveEnvironment();
325+
if (activeEnv?.apiKey && isUnclaimedEnvironment(activeEnv)) {
326+
return true;
327+
}
328+
324329
const token = getAccessToken();
325330
return token !== null;
326331
}),

0 commit comments

Comments
 (0)