chore: apply requested changes (#1442)

This commit is contained in:
Ralph Khreish
2025-11-24 23:00:13 +01:00
committed by GitHub
parent 0195feffee
commit 01d4d9930f
7 changed files with 53 additions and 17 deletions

View File

@@ -177,3 +177,4 @@ Apply standard software engineering principles:
- **Add a changeset for code changes** - Run `npx changeset` after making code changes (not needed for docs-only PRs)
- When creating changesets, remember that it's user-facing, meaning we don't have to get into the specifics of the code, but rather mention what the end-user is getting or fixing from this changeset
- Run `npm run turbo:typecheck` before pushing to ensure TypeScript type checks pass
- Run `npm run test -w <package-name>` to test a package

View File

@@ -551,13 +551,15 @@ Examples:
private async handleMFAVerification(
mfaError: AuthenticationError
): Promise<AuthCredentials> {
if (!mfaError.mfaChallenge) {
if (!mfaError.mfaChallenge?.factorId) {
throw new AuthenticationError(
'MFA challenge information missing',
'MFA_VERIFICATION_FAILED'
);
}
const { factorId } = mfaError.mfaChallenge;
console.log(
chalk.yellow(
'\n⚠ Multi-factor authentication is enabled on your account'
@@ -571,7 +573,7 @@ Examples:
// Use @tm/core's retry logic - presentation layer just handles UI
const result = await this.authManager.verifyMFAWithRetry(
mfaError.mfaChallenge.factorId,
factorId,
async () => {
// Prompt for MFA code
try {

View File

@@ -339,7 +339,9 @@ describe('AuthManager - MFA Retry Logic', () => {
authManager.verifyMFAWithRetry('factor-123', codeProvider, {
maxAttempts: 0
})
).rejects.toThrow('Invalid maxAttempts value: 0. Must be at least 1.');
).rejects.toThrow(
'Invalid maxAttempts value: 0. Must be a positive integer.'
);
// Test with negative
await expect(
@@ -352,7 +354,9 @@ describe('AuthManager - MFA Retry Logic', () => {
authManager.verifyMFAWithRetry('factor-123', codeProvider, {
maxAttempts: -1
})
).rejects.toThrow('Invalid maxAttempts value: -1. Must be at least 1.');
).rejects.toThrow(
'Invalid maxAttempts value: -1. Must be a positive integer.'
);
// Verify code provider was never called
expect(codeProvider).not.toHaveBeenCalled();

View File

@@ -163,9 +163,13 @@ export class AuthManager {
const onInvalidCode = options?.onInvalidCode;
// Guard against invalid maxAttempts values
if (maxAttempts < 1) {
if (
!Number.isFinite(maxAttempts) ||
!Number.isInteger(maxAttempts) ||
maxAttempts < 1
) {
throw new TypeError(
`Invalid maxAttempts value: ${maxAttempts}. Must be at least 1.`
`Invalid maxAttempts value: ${maxAttempts}. Must be a positive integer.`
);
}

View File

@@ -397,17 +397,16 @@ describe('SessionManager', () => {
mockContextStore as any
);
try {
await sessionManager.authenticateWithCode('test-code');
expect.fail('Should have thrown MFA_REQUIRED error');
} catch (error) {
expect(error).toBeInstanceOf(AuthenticationError);
expect((error as AuthenticationError).code).toBe('MFA_REQUIRED');
expect((error as AuthenticationError).mfaChallenge).toEqual({
const promise = sessionManager.authenticateWithCode('test-code');
await expect(promise).rejects.toThrow(AuthenticationError);
await expect(promise).rejects.toMatchObject({
code: 'MFA_REQUIRED',
mfaChallenge: {
factorId: 'factor-123',
factorType: 'totp'
});
}
}
});
});
});

View File

@@ -42,7 +42,10 @@ export class SessionManager {
await this.migrateLegacyAuth();
} catch (error) {
// Log but don't throw - initialization errors are handled gracefully
this.logger.debug('Session initialization completed with warnings');
this.logger.debug(
'Session initialization completed with warnings',
error
);
}
}
@@ -102,7 +105,7 @@ export class SessionManager {
await this.waitForInitialization();
try {
const session = await this.supabaseClient.getSession();
return session !== null;
return session != null;
} catch {
return false;
}

View File

@@ -37,7 +37,15 @@ describe('SupabaseAuthClient', () => {
let authClient: InstanceType<typeof SupabaseAuthClient>;
let mockSupabaseClient: any;
// Store original env values for cleanup
let originalSupabaseUrl: string | undefined;
let originalSupabaseAnonKey: string | undefined;
beforeEach(() => {
// Store original values
originalSupabaseUrl = process.env.TM_SUPABASE_URL;
originalSupabaseAnonKey = process.env.TM_SUPABASE_ANON_KEY;
// Set required environment variables
process.env.TM_SUPABASE_URL = 'https://test.supabase.co';
process.env.TM_SUPABASE_ANON_KEY = 'test-anon-key';
@@ -67,6 +75,21 @@ describe('SupabaseAuthClient', () => {
vi.clearAllMocks();
});
afterEach(() => {
// Restore original env values
if (originalSupabaseUrl === undefined) {
delete process.env.TM_SUPABASE_URL;
} else {
process.env.TM_SUPABASE_URL = originalSupabaseUrl;
}
if (originalSupabaseAnonKey === undefined) {
delete process.env.TM_SUPABASE_ANON_KEY;
} else {
process.env.TM_SUPABASE_ANON_KEY = originalSupabaseAnonKey;
}
});
describe('verifyMFA', () => {
it('should verify MFA and refresh session to upgrade to AAL2', async () => {
// Mock the challenge response