diff --git a/app/(main)/admin/__tests__/marketplace-install-version.test.tsx b/app/(main)/admin/__tests__/marketplace-install-version.test.tsx index b8efa0cef..4377ec655 100644 --- a/app/(main)/admin/__tests__/marketplace-install-version.test.tsx +++ b/app/(main)/admin/__tests__/marketplace-install-version.test.tsx @@ -49,6 +49,36 @@ describe('installVersion', () => { expect(calls).toHaveLength(1); }); + it('says what an update adds, and repeats with what the admin saw', async () => { + responses = [json({ error: 'adds', permissionsAdded: ['keystore'] }, 409), json({ warnings: [] })]; + const asked: string[] = []; + const result = await installVersion(BODY, (q) => { asked.push(`${q.title} ${q.message}`); return true; }); + expect(asked[0]).toContain('new permissions'); + expect(asked[0]).toContain('keystore'); + expect(calls.map((c) => c.body.acceptPermissions)).toEqual([undefined, ['keystore']]); + expect(calls.map((c) => c.body.force), 'not the same as forcing it').toEqual([undefined, undefined]); + expect(result.ok).toBe(true); + }); + + it('does not update past new permissions the admin declined', async () => { + responses = [json({ error: 'adds', permissionsAdded: ['keystore'] }, 409)]; + expect(await installVersion(BODY, () => false)).toEqual({ ok: false, error: 'Not installed.' }); + expect(calls).toHaveLength(1); + }); + + it('asks about both when a withdrawn version also adds permissions, and keeps both answers', async () => { + responses = [ + json({ error: 'withdrawn', yanked: { reason: null } }, 409), + json({ error: 'adds', permissionsAdded: ['keystore'] }, 409), + json({ warnings: [] }), + ]; + const asked: string[] = []; + const result = await installVersion(BODY, (q) => { asked.push(q.title); return true; }); + expect(asked).toHaveLength(2); + expect(calls.at(-1)!.body).toMatchObject({ force: true, acceptPermissions: ['keystore'] }); + expect(result.ok).toBe(true); + }); + it('shows any other 409 as the error it is', async () => { responses = [json({ error: 'The configuration is read-only' }, 409)]; const result = await installVersion(BODY, () => true); diff --git a/app/(main)/admin/_pages/marketplace-detail.tsx b/app/(main)/admin/_pages/marketplace-detail.tsx index fdb7b54dd..a55a8ffcb 100644 --- a/app/(main)/admin/_pages/marketplace-detail.tsx +++ b/app/(main)/admin/_pages/marketplace-detail.tsx @@ -28,6 +28,7 @@ import { apiFetch } from '@/lib/browser-navigation'; import { compareVersions, isVersionSatisfied } from '@/lib/version-compare'; import type { MarketplaceListing } from '@/lib/admin/marketplace-directory'; import { adminPath } from '@/lib/admin/admin-nav'; +import { declaredPermissions, HIGH_RISK_PERMISSIONS } from '@/lib/plugin-sandbox/permission-text'; import { cn } from '@/lib/utils'; import { SettingsSection } from '@/components/settings/settings-section'; import { Button } from '@/components/ui/button'; @@ -97,6 +98,8 @@ interface PreviewData { requestedVersionMissing: string | null; bundle: { manifest: Record | null; + /** What this version asks for that the installed one doesn't. */ + addedPermissions?: string[]; source: { name: string; content: string; truncated: boolean } | null; size: number; error: string | null; @@ -307,8 +310,10 @@ export function MarketplaceDetailPage({ slug }: { slug: string }) { const bundle = data.bundle; const preview = data.preview; const isPlugin = ext.type === 'plugin'; - const manifestPerms = (bundle.manifest?.permissions as string[] | undefined) || preview && ext.versions.find((v) => v.version === preview.version)?.permissions || ext.permissions || []; + // Optional permissions are granted like the others, so they count here too. + const manifestPerms = bundle.manifest ? declaredPermissions(bundle.manifest) : (preview && ext.versions.find((v) => v.version === preview.version)?.permissions) || ext.permissions || []; const permissionText = new Map(ext.permissionDetails.map((p) => [p.id, p])); + const addedPermissions = new Set(bundle.addedPermissions ?? []); const frameOrigins = (bundle.manifest?.frameOrigins as string[] | undefined) || []; const settingsSchema = bundle.manifest?.settingsSchema as Record | undefined; @@ -705,7 +710,7 @@ export function MarketplaceDetailPage({ slug }: { slug: string }) {