fix: Scope GitHub skill identity lookup by tenant

This commit is contained in:
Danny Avila 2026-05-30 15:49:51 -07:00
parent c7275bd945
commit 356012f699
4 changed files with 59 additions and 1 deletions

View file

@ -463,6 +463,11 @@ describe('createGitHubSkillSyncRunner', () => {
expect(result.status).toBe('completed');
expect(observedTenantId).toBe('tenant-a');
expect(deps.findSkillBySourceIdentity).toHaveBeenCalledWith({
source: 'github',
upstreamId: 'librechat-skills:skills/research',
tenantId: 'tenant-a',
});
expect(deps.createSkill).toHaveBeenCalledWith(
expect.objectContaining({ name: 'research', tenantId: 'tenant-a' }),
);
@ -778,6 +783,7 @@ describe('createGitHubSkillSyncRunner', () => {
expect(deps.findSkillBySourceIdentity).toHaveBeenCalledWith({
source: 'github',
upstreamId: 'librechat-skills:skills/research',
tenantId: undefined,
});
expect(deps.createSkill).not.toHaveBeenCalled();
expect(deps.updateSkill).toHaveBeenCalledWith(

View file

@ -157,6 +157,7 @@ export type GitHubSkillSyncDeps = {
findSkillBySourceIdentity: (params: {
source: 'github' | 'notion';
upstreamId: string;
tenantId?: string;
}) => Promise<(ISkill & { _id: Types.ObjectId }) | null>;
listSkillsBySource: (params: {
source: 'github' | 'notion';
@ -803,8 +804,12 @@ async function prepareRemoteSkill(params: {
source: PROVIDER,
sourceMetadata,
};
const foundExisting = await deps.findSkillBySourceIdentity({ source: PROVIDER, upstreamId });
const sourceTenantId = source.tenantId ?? undefined;
const foundExisting = await deps.findSkillBySourceIdentity({
source: PROVIDER,
upstreamId,
tenantId: sourceTenantId,
});
const existing =
foundExisting && (foundExisting.tenantId ?? undefined) === sourceTenantId
? foundExisting

View file

@ -507,6 +507,48 @@ describe('Skill CRUD methods', () => {
expect(await SkillFile.countDocuments({ skillId: skill._id })).toBe(0);
});
it('findSkillBySourceIdentity searches only the requested tenant bucket', async () => {
const upstreamId = 'librechat-skills:skills/research';
const sourceMetadata = {
provider: 'github',
sourceId: 'librechat-skills',
upstreamId,
};
const author = new mongoose.Types.ObjectId();
const tenantSkill = await Skill.create({
name: 'research',
description: 'A tenant-scoped GitHub skill mirror.',
body: 'tenant body',
author,
authorName: 'GitHub Sync',
tenantId: 'tenant-a',
source: 'github',
sourceMetadata,
});
const ambientSkill = await Skill.create({
name: 'research',
description: 'An ambient GitHub skill mirror.',
body: 'ambient body',
author,
authorName: 'GitHub Sync',
source: 'github',
sourceMetadata,
});
const ambientResult = await methods.findSkillBySourceIdentity({
source: 'github',
upstreamId,
});
const tenantResult = await methods.findSkillBySourceIdentity({
source: 'github',
upstreamId,
tenantId: 'tenant-a',
});
expect(ambientResult?._id.toString()).toBe(ambientSkill._id.toString());
expect(tenantResult?._id.toString()).toBe(tenantSkill._id.toString());
});
it('listSkillsByAccess returns only accessible skills and paginates by cursor', async () => {
const ids: mongoose.Types.ObjectId[] = [];
for (let i = 0; i < 3; i++) {

View file

@ -1365,11 +1365,16 @@ export function createSkillMethods(mongoose: typeof import('mongoose'), deps: Sk
async function findSkillBySourceIdentity(params: {
source: 'github' | 'notion';
upstreamId: string;
tenantId?: string;
}): Promise<(ISkill & { _id: Types.ObjectId }) | null> {
const Skill = mongoose.models.Skill as Model<ISkillDocument>;
const tenantFilter: FilterQuery<ISkillDocument> = params.tenantId
? { tenantId: params.tenantId }
: { $or: [{ tenantId: { $exists: false } }, { tenantId: null }] };
const doc = await Skill.findOne({
source: params.source,
'sourceMetadata.upstreamId': params.upstreamId,
...tenantFilter,
}).lean();
return (doc as unknown as (ISkill & { _id: Types.ObjectId }) | null) ?? null;
}