fix(auth): harden continuity inheritance resolution

- resolve mappings with alias/canonical profile candidates

- fail open when source account initialization errors

- remove metadata touch side effects from inheritance preflight
This commit is contained in:
Tam Nhu Tran
2026-03-03 02:49:17 +07:00
parent 0b9f9826e2
commit 8a45727b31
2 changed files with 160 additions and 51 deletions
+65 -33
View File
@@ -9,6 +9,7 @@ import InstanceManager from '../management/instance-manager';
import ProfileRegistry from './profile-registry'; import ProfileRegistry from './profile-registry';
import { isAccountContextMetadata, resolveAccountContextPolicy } from './account-context'; import { isAccountContextMetadata, resolveAccountContextPolicy } from './account-context';
import type { ProfileType } from '../types/profile'; import type { ProfileType } from '../types/profile';
import { getProfileLookupCandidates, resolveAliasToCanonical } from '../utils/profile-compat';
export interface ProfileContinuityInheritanceInput { export interface ProfileContinuityInheritanceInput {
profileName: string; profileName: string;
@@ -50,7 +51,14 @@ function loadLegacyContinuityInheritanceMap(): Record<string, string> {
} }
return normalized; return normalized;
} catch { } catch (error) {
if (process.env.CCS_DEBUG) {
console.error(
warn(
`Failed to parse legacy continuity mapping at "${configJsonPath}": ${(error as Error).message}`
)
);
}
return {}; return {};
} }
} }
@@ -59,13 +67,38 @@ function resolveMappedAccount(
profileName: string, profileName: string,
inheritFromAccount: Record<string, string> inheritFromAccount: Record<string, string>
): string | undefined { ): string | undefined {
const mapped = inheritFromAccount[profileName]; const candidates = new Set<string>([
if (typeof mapped !== 'string') { ...getProfileLookupCandidates(profileName),
return undefined; resolveAliasToCanonical(profileName),
]);
for (const candidate of candidates) {
const mapped = inheritFromAccount[candidate];
if (typeof mapped !== 'string') {
continue;
}
const normalized = mapped.trim();
if (normalized.length > 0) {
return normalized;
}
} }
const normalized = mapped.trim(); const normalizedCandidates = new Set(
return normalized.length > 0 ? normalized : undefined; [...candidates].map((candidate) => candidate.trim().toLowerCase()).filter(Boolean)
);
for (const [mappedProfileName, mappedAccountName] of Object.entries(inheritFromAccount)) {
if (!normalizedCandidates.has(mappedProfileName.trim().toLowerCase())) {
continue;
}
const normalized = mappedAccountName.trim();
if (normalized.length > 0) {
return normalized;
}
}
return undefined;
} }
/** /**
@@ -93,37 +126,36 @@ export async function resolveProfileContinuityInheritance(
return {}; return {};
} }
const registry = new ProfileRegistry(); try {
const profiles = registry.getAllProfilesMerged(); const registry = new ProfileRegistry();
const mappedProfile = profiles[sourceAccount]; const profiles = registry.getAllProfilesMerged();
if (!mappedProfile || mappedProfile.type !== 'account') { const mappedProfile = profiles[sourceAccount];
if (!mappedProfile || mappedProfile.type !== 'account') {
console.error(
warn(
`Continuity inheritance skipped for "${input.profileName}": source "${sourceAccount}" not found or not an account profile`
)
);
return {};
}
const contextPolicy = resolveAccountContextPolicy(
isAccountContextMetadata(mappedProfile) ? mappedProfile : undefined
);
const instanceMgr = new InstanceManager();
const instancePath = await instanceMgr.ensureInstance(sourceAccount, contextPolicy);
return {
sourceAccount,
claudeConfigDir: instancePath,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error( console.error(
warn( warn(
`Continuity inheritance skipped for "${input.profileName}": source account "${sourceAccount}" not found` `Continuity inheritance skipped for "${input.profileName}": failed to initialize source "${sourceAccount}" (${message})`
) )
); );
return {}; return {};
} }
const contextPolicy = resolveAccountContextPolicy(
isAccountContextMetadata(mappedProfile) ? mappedProfile : undefined
);
const instanceMgr = new InstanceManager();
const instancePath = await instanceMgr.ensureInstance(sourceAccount, contextPolicy);
// Best-effort touch only; execution must continue even if touch fails.
try {
if (registry.hasAccountUnified(sourceAccount)) {
registry.touchAccountUnified(sourceAccount);
} else if (registry.hasProfile(sourceAccount)) {
registry.touchProfile(sourceAccount);
}
} catch {
// Ignore metadata touch failure.
}
return {
sourceAccount,
claudeConfigDir: instancePath,
};
} }
@@ -45,13 +45,6 @@ describe('resolveProfileContinuityInheritance', () => {
}, },
} }
); );
const hasUnifiedSpy = spyOn(ProfileRegistry.prototype, 'hasAccountUnified').mockReturnValue(true);
const touchUnifiedSpy = spyOn(ProfileRegistry.prototype, 'touchAccountUnified').mockImplementation(
() => undefined
);
const touchLegacySpy = spyOn(ProfileRegistry.prototype, 'touchProfile').mockImplementation(
() => undefined
);
const result = await resolveProfileContinuityInheritance({ const result = await resolveProfileContinuityInheritance({
profileName: 'glm', profileName: 'glm',
@@ -64,9 +57,6 @@ describe('resolveProfileContinuityInheritance', () => {
claudeConfigDir: '/tmp/.ccs/instances/pro', claudeConfigDir: '/tmp/.ccs/instances/pro',
}); });
expect(getProfilesSpy).toHaveBeenCalledTimes(1); expect(getProfilesSpy).toHaveBeenCalledTimes(1);
expect(hasUnifiedSpy).toHaveBeenCalledWith('pro');
expect(touchUnifiedSpy).toHaveBeenCalledWith('pro');
expect(touchLegacySpy).not.toHaveBeenCalled();
expect(ensureInstanceSpy).toHaveBeenCalledWith('pro', { expect(ensureInstanceSpy).toHaveBeenCalledWith('pro', {
mode: 'shared', mode: 'shared',
group: 'team-alpha', group: 'team-alpha',
@@ -101,11 +91,6 @@ describe('resolveProfileContinuityInheritance', () => {
last_used: null, last_used: null,
}, },
}); });
spyOn(ProfileRegistry.prototype, 'hasAccountUnified').mockReturnValue(false);
spyOn(ProfileRegistry.prototype, 'hasProfile').mockReturnValue(true);
const touchLegacySpy = spyOn(ProfileRegistry.prototype, 'touchProfile').mockImplementation(
() => undefined
);
spyOn(InstanceManager.prototype, 'ensureInstance').mockResolvedValue('/tmp/.ccs/instances/work'); spyOn(InstanceManager.prototype, 'ensureInstance').mockResolvedValue('/tmp/.ccs/instances/work');
const result = await resolveProfileContinuityInheritance({ const result = await resolveProfileContinuityInheritance({
@@ -118,7 +103,6 @@ describe('resolveProfileContinuityInheritance', () => {
sourceAccount: 'work', sourceAccount: 'work',
claudeConfigDir: '/tmp/.ccs/instances/work', claudeConfigDir: '/tmp/.ccs/instances/work',
}); });
expect(touchLegacySpy).toHaveBeenCalledWith('work');
}); });
it('returns empty result when mapped source account does not exist', async () => { it('returns empty result when mapped source account does not exist', async () => {
@@ -195,8 +179,6 @@ describe('resolveProfileContinuityInheritance', () => {
last_used: null, last_used: null,
}, },
}); });
spyOn(ProfileRegistry.prototype, 'hasAccountUnified').mockReturnValue(true);
spyOn(ProfileRegistry.prototype, 'touchAccountUnified').mockImplementation(() => undefined);
spyOn(InstanceManager.prototype, 'ensureInstance').mockResolvedValue('/tmp/.ccs/instances/pro'); spyOn(InstanceManager.prototype, 'ensureInstance').mockResolvedValue('/tmp/.ccs/instances/pro');
const result = await resolveProfileContinuityInheritance({ const result = await resolveProfileContinuityInheritance({
@@ -210,4 +192,99 @@ describe('resolveProfileContinuityInheritance', () => {
claudeConfigDir: '/tmp/.ccs/instances/pro', claudeConfigDir: '/tmp/.ccs/instances/pro',
}); });
}); });
it('returns empty result when mapped source exists but is not an account profile', async () => {
spyOn(configLoader, 'loadOrCreateUnifiedConfig').mockReturnValue({
version: 8,
continuity: {
inherit_from_account: {
glm: 'settings-profile',
},
},
} as ReturnType<typeof configLoader.loadOrCreateUnifiedConfig>);
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({
'settings-profile': {
type: 'settings',
created: '2026-03-01T00:00:00.000Z',
last_used: null,
},
});
const ensureInstanceSpy = spyOn(InstanceManager.prototype, 'ensureInstance');
const result = await resolveProfileContinuityInheritance({
profileName: 'glm',
profileType: 'settings',
target: 'claude',
});
expect(result).toEqual({});
expect(ensureInstanceSpy).not.toHaveBeenCalled();
});
it('supports profile alias lookup when continuity mapping uses legacy key', async () => {
spyOn(configLoader, 'loadOrCreateUnifiedConfig').mockReturnValue({
version: 8,
continuity: {
inherit_from_account: {
kimi: 'pro',
},
},
} as ReturnType<typeof configLoader.loadOrCreateUnifiedConfig>);
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({
pro: {
type: 'account',
created: '2026-03-01T00:00:00.000Z',
last_used: null,
},
});
const ensureInstanceSpy = spyOn(InstanceManager.prototype, 'ensureInstance').mockResolvedValue(
'/tmp/.ccs/instances/pro'
);
const result = await resolveProfileContinuityInheritance({
profileName: 'km',
profileType: 'settings',
target: 'claude',
});
expect(result).toEqual({
sourceAccount: 'pro',
claudeConfigDir: '/tmp/.ccs/instances/pro',
});
expect(ensureInstanceSpy).toHaveBeenCalledWith('pro', {
mode: 'isolated',
});
});
it('fails open when source account instance initialization throws', async () => {
spyOn(configLoader, 'loadOrCreateUnifiedConfig').mockReturnValue({
version: 8,
continuity: {
inherit_from_account: {
glm: 'pro',
},
},
} as ReturnType<typeof configLoader.loadOrCreateUnifiedConfig>);
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({
pro: {
type: 'account',
created: '2026-03-01T00:00:00.000Z',
last_used: null,
},
});
spyOn(InstanceManager.prototype, 'ensureInstance').mockRejectedValue(
new Error('instance init failed')
);
const result = await resolveProfileContinuityInheritance({
profileName: 'glm',
profileType: 'settings',
target: 'claude',
});
expect(result).toEqual({});
});
}); });