From 27e84b3c93c9c1797436ea7aa2840f94584df4ee Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Mon, 2 Nov 2026 23:33:57 +0100
Subject: [PATCH] fix: show the app name and plugin name in a marketplace
plugin's permissions
---
.../admin/_pages/marketplace-detail.tsx | 2 +-
app/api/admin/marketplace/[slug]/route.ts | 6 +++++-
.../marketplace/__tests__/preview.test.ts | 21 +++++++++++++++++--
3 files changed, 25 insertions(+), 4 deletions(-)
diff --git a/app/(main)/admin/_pages/marketplace-detail.tsx b/app/(main)/admin/_pages/marketplace-detail.tsx
index db20ab2d1..9f528b80f 100644
--- a/app/(main)/admin/_pages/marketplace-detail.tsx
+++ b/app/(main)/admin/_pages/marketplace-detail.tsx
@@ -693,7 +693,7 @@ export function MarketplaceDetailPage({ slug }: { slug: string }) {
title="Permissions"
description={manifestPerms.length === 0
? 'This plugin requests no permissions.'
- : `What ${preview ? `v${preview.version}` : 'the plugin'} may do in each user's mailbox.`}
+ : `What ${ext.name}${preview ? ` ${preview.version}` : ''} may do in each user's mailbox.`}
>
{manifestPerms.length > 0 && (
diff --git a/app/api/admin/marketplace/[slug]/route.ts b/app/api/admin/marketplace/[slug]/route.ts
index e53a2a545..91fb8509a 100644
--- a/app/api/admin/marketplace/[slug]/route.ts
+++ b/app/api/admin/marketplace/[slug]/route.ts
@@ -24,6 +24,8 @@ import {
type DirectoryVersion,
} from '@/lib/admin/marketplace-directory';
import { MAX_PLUGIN_SIZE, MAX_THEME_SIZE } from '@/lib/plugin-types';
+import { productName } from '@/lib/product-name';
+import { withAppName } from '@/lib/settings-search';
const MAX_PREVIEW_SOURCE_LEN = 100_000;
@@ -173,6 +175,7 @@ export async function GET(
const installed = await installedExtensions();
const state = installedStateOf(installed, slug, type);
const history = await historyFor(type, slug);
+ const appName = productName(configManager.get('appName'));
return NextResponse.json(
{
@@ -185,7 +188,8 @@ export async function GET(
screenshots: detail.screenshots ?? [],
themePreviews: detail.themePreviews ?? [],
advisories: detail.advisories ?? [],
- permissionDetails: detail.permissions ?? [],
+ // The directory ships the webmail's permission texts unfilled.
+ permissionDetails: (detail.permissions ?? []).map((p) => withAppName(p, appName) as typeof p),
source: detail.source,
createdAt: detail.createdAt ?? null,
updatedAt: detail.updatedAt ?? null,
diff --git a/app/api/admin/marketplace/__tests__/preview.test.ts b/app/api/admin/marketplace/__tests__/preview.test.ts
index ca4d8897b..579aff6e5 100644
--- a/app/api/admin/marketplace/__tests__/preview.test.ts
+++ b/app/api/admin/marketplace/__tests__/preview.test.ts
@@ -5,6 +5,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
import { NextRequest } from 'next/server';
import JSZip from 'jszip';
+const appName = vi.hoisted(() => ({ value: 'Webmail' }));
+
vi.mock('@/lib/admin/session', () => ({
requireAdminAuth: vi.fn(async () => ({ session: { sub: 'admin' } })),
getClientIP: () => '127.0.0.1',
@@ -12,7 +14,7 @@ vi.mock('@/lib/admin/session', () => ({
vi.mock('@/lib/admin/config-manager', () => ({
configManager: {
ensureLoaded: vi.fn(async () => {}),
- get: vi.fn((key: string) => (key === 'extensionDirectoryUrl' ? 'https://extensions.test' : undefined)),
+ get: vi.fn((key: string) => (key === 'extensionDirectoryUrl' ? 'https://extensions.test' : key === 'appName' ? appName.value : undefined)),
},
}));
vi.mock('@/lib/admin/plugin-registry', () => ({
@@ -54,7 +56,9 @@ function directory() {
const u = new URL(String(url));
calls.push(u.pathname + u.search);
if (u.pathname === '/api/v2/extensions/pretty-dates') {
- return Response.json({ data: { ...summary, latest: versions[0], newest: versions[0], longDescription: 'long', versions: versions.slice(0, 2), screenshots: [], themePreviews: [], advisories: [], permissions: [] } });
+ return Response.json({ data: { ...summary, latest: versions[0], newest: versions[0], longDescription: 'long', versions: versions.slice(0, 2), screenshots: [], themePreviews: [], advisories: [], permissions: [
+ { id: 'http:proxy', title: 'Use accounts you connect', body: "Send requests through {appName}'s server.", highRisk: true },
+ ] } });
}
if (u.pathname === '/api/v2/extensions/pretty-dates/versions') return Response.json({ data: versions });
const m = /\/versions\/([^/]+)\/bundle$/.exec(u.pathname);
@@ -105,6 +109,19 @@ describe('GET /api/admin/marketplace/[slug]', () => {
expect(body.preview.version).toBe('1.2.0');
});
+ it('fills the app name into the permission texts the directory sends', async () => {
+ directory();
+ appName.value = 'Acme Mail';
+ let body = await (await preview()).json();
+ expect(body.extension.permissionDetails).toEqual([
+ { id: 'http:proxy', title: 'Use accounts you connect', body: "Send requests through Acme Mail's server.", highRisk: true },
+ ]);
+
+ appName.value = 'Webmail';
+ body = await (await preview()).json();
+ expect(body.extension.permissionDetails[0].body).toBe("Send requests through Bulwark's server.");
+ });
+
it('refuses a malformed version instead of forwarding it', async () => {
directory();
expect((await preview('?version=..%2F..%2Fetc')).status).toBe(400);