fix: serve plugin bundles only to signed-in users

This commit is contained in:
Linus Rath
2026-10-14 02:27:35 +02:00
parent c4853a4bba
commit 15cec58a46
2 changed files with 43 additions and 1 deletions
@@ -0,0 +1,36 @@
/**
* @vitest-environment node
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { NextRequest } from 'next/server';
let signedIn = false;
vi.mock('@/lib/auth/instance-details', () => ({ canSeeInstanceDetails: vi.fn(async () => signedIn) }));
vi.mock('@/lib/admin/plugin-dev', () => ({ getDevPlugin: vi.fn(async () => null), readDevBundle: vi.fn() }));
vi.mock('@/lib/admin/plugin-registry', () => ({
getPlugin: vi.fn(async () => ({ id: 'demo', enabled: true, bundleHash: 'h' })),
getPluginBundle: vi.fn(async () => 'module.exports = {}'),
getPluginSignature: vi.fn(async () => 'sig'),
}));
vi.mock('@/lib/admin/plugin-signing', () => ({ signBytes: vi.fn(async () => 'sig') }));
import { GET } from '../route';
function get() {
return GET(new NextRequest('http://localhost/api/admin/plugins/demo/bundle'), { params: Promise.resolve({ id: 'demo' }) });
}
beforeEach(() => { signedIn = false; });
describe('GET plugin bundle', () => {
it('does not hand plugin code to someone who is not signed in', async () => {
expect((await get()).status).toBe(401);
});
it('serves it to a signed-in user with its signature', async () => {
signedIn = true;
const res = await get();
expect(res.status).toBe(200);
expect(res.headers.get('x-bundle-signature')).toBe('sig');
});
});
+7 -1
View File
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { getPluginBundle, getPlugin, getPluginSignature } from '@/lib/admin/plugin-registry';
import { getDevPlugin, readDevBundle } from '@/lib/admin/plugin-dev';
import { signBytes } from '@/lib/admin/plugin-signing';
import { canSeeInstanceDetails } from '@/lib/auth/instance-details';
async function safeSign(code: string): Promise<string | null> {
try { return await signBytes(code); } catch { return null; }
@@ -10,13 +11,18 @@ async function safeSign(code: string): Promise<string | null> {
/**
* GET /api/admin/plugins/[id]/bundle - Serve plugin JS bundle
*
* Public endpoint so the client-side plugin loader can fetch bundles.
* Serves bundles to signed-in users so the client-side plugin loader can fetch them.
* Only serves plugins that exist in the registry and are enabled.
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
// Bundles are delivered to signed-in users only, like the plugin list
// that names them (/api/plugins).
if (!(await canSeeInstanceDetails(request))) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
try {
const { id } = await params;