fix: reject unsupported exact project source

This commit is contained in:
lefarcen
2026-08-12 22:35:00 +08:00
parent 33b36688c7
commit 99e0cf23c1
2 changed files with 42 additions and 1 deletions
+5 -1
View File
@@ -3515,9 +3515,13 @@ export function registerProjectRoutes(app: Express, ctx: RegisterProjectRoutesDe
if (
requestedPluginId
&& requestedPluginSource
&& ctx.pluginScope?.getLocalPluginBySource
&& !exactLocalPlugin
) {
// PRODUCT INVARIANT: once the client supplies an exact local source,
// never degrade to an ID-only resolver. Older route environments that
// lack source-aware resolution cannot prove which same-id record was
// selected, so they must fail closed without creating a project. This
// is a local compatibility fence, not login or Workspace authority.
return sendApiError(res, 404, 'PLUGIN_NOT_FOUND', 'plugin not found');
}
if (requestedPluginId) {
@@ -429,4 +429,41 @@ describe('project resource selection uses the persisted exact member', () => {
expect(getLocalPluginBySource).toHaveBeenCalledWith('shared-id', source);
expect(insertProject).not.toHaveBeenCalled();
});
it('does not let a legacy plugin scope substitute an ID-only record for an exact source', async () => {
const insertProject = vi.fn((_: unknown, input: Record<string, unknown>) => input);
const insertConversation = vi.fn();
const loadRegistry = vi.fn();
const getPlugin = vi.fn(async () => ({
id: 'shared-id',
source: 'team:plugin:current-workspace:shared-id',
}));
const baseUrl = await start(buildDeps({
insertProject,
insertConversation,
loadRegistry,
getPlugin,
// Deliberately omit getLocalPluginBySource to model a legacy scope.
}));
const response = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: headers(),
body: JSON.stringify({
id: 'legacy-scope-exact-source',
name: 'Legacy scope exact source',
pluginId: 'shared-id',
pluginSource: 'local:personal:shared-id',
}),
});
expect(response.status).toBe(404);
await expect(response.json()).resolves.toMatchObject({
error: { code: 'PLUGIN_NOT_FOUND' },
});
expect(getPlugin).not.toHaveBeenCalled();
expect(loadRegistry).not.toHaveBeenCalled();
expect(insertProject).not.toHaveBeenCalled();
expect(insertConversation).not.toHaveBeenCalled();
});
});