/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * `CapabilityReview` — modal dialog the user must confirm before any * extension is installed. * * Sources the capability list from the bundle's manifest, parses each * capability, runs the risk classifier, and surfaces a per-row badge * with a plain-English description. The user can: * * - Approve every capability (default). * - Uncheck individual capabilities they don't want to grant. * The host enforces these at runtime via the inner-ring check; * extensions that need them fail visibly. * - Cancel. * * For red-tier capabilities we require the user to type "approve" as * a friction layer — matching the threat-model recommendation in * `02-security.md §4`. * * The dialog is purely presentational: it returns a grant decision to * the parent via `onApprove(grants)` / `onCancel()`. The parent is * responsible for calling `host.installFromBytes(bytes, grants)`. */ import { useMemo, useState } from 'react'; import { AlertTriangle, CheckCircle2, FileCode2, ShieldAlert, ShieldCheck, X, KeyRound, Unlock } from 'lucide-react'; import { BundlePreview } from './BundlePreview'; import { computeRisks, overallTier, parseCapability, type Capability, type CapabilityRisk, type RiskTier, } from '@ifc-lite/extensions'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { ScrollArea } from '@/components/ui/scroll-area'; import { cn } from '@/lib/utils'; import type { ExtensionInstallSummary } from '@/services/extensions/host.js'; interface CapabilityReviewProps { open: boolean; summary: ExtensionInstallSummary; /** When supplied, render the capability diff vs the previously-granted set. */ previousGrants?: readonly string[]; /** Optional previous version label (e.g. "v1.2.0") for the diff banner. */ previousVersion?: string; onApprove(grants: string[]): void; onCancel(): void; } interface CapabilityRow { raw: string; capability: Capability | null; risk: CapabilityRisk | null; } const APPROVE_PHRASE = 'approve'; export function CapabilityReview({ open, summary, previousGrants, previousVersion, onApprove, onCancel, }: CapabilityReviewProps) { const rows = useMemo(() => { return summary.capabilities.map((raw) => { const parsed = parseCapability(raw); if (!parsed.ok) return { raw, capability: null, risk: null }; const [risk] = computeRisks([parsed.value]); return { raw, capability: parsed.value, risk }; }); }, [summary]); const overall = useMemo(() => { return overallTier(rows.map((r) => r.risk).filter((r): r is CapabilityRisk => !!r)); }, [rows]); /** Capability strings introduced since the previous install, if any. */ const newSinceUpgrade = useMemo>(() => { if (!previousGrants) return new Set(); const prior = new Set(previousGrants); return new Set(summary.capabilities.filter((c) => !prior.has(c))); }, [previousGrants, summary.capabilities]); /** Capability strings the new bundle no longer requests. */ const droppedSinceUpgrade = useMemo(() => { if (!previousGrants) return []; const next = new Set(summary.capabilities); return previousGrants.filter((c) => !next.has(c)); }, [previousGrants, summary.capabilities]); const [granted, setGranted] = useState>( () => new Set(summary.capabilities), ); const [confirmText, setConfirmText] = useState(''); const [tab, setTab] = useState<'capabilities' | 'source'>('capabilities'); const needsConfirm = useMemo(() => { for (const row of rows) { if (row.risk?.tier === 'red' && granted.has(row.raw)) return true; } return false; }, [rows, granted]); const canApprove = !needsConfirm || confirmText.trim().toLowerCase() === APPROVE_PHRASE; const toggle = (raw: string, checked: boolean) => { setGranted((prev) => { const next = new Set(prev); if (checked) next.add(raw); else next.delete(raw); return next; }); }; return ( { if (!o) onCancel(); }}>
Install {summary.id} v{summary.version}?
Review the capabilities this extension is requesting. Uncheck any you do not want to grant. Extensions that rely on a denied capability will surface a clear error at runtime instead of running silently with broader scope.
{summary.signed && summary.signature ? (
Signature verified
Signed by {summary.signature.fingerprint.slice(0, 23)}… {' · '} {new Date(summary.signature.signedAt).toLocaleString()}
) : (
Unsigned bundle
This bundle has no signature. We cannot verify it came from a specific publisher — install only if you trust the source.
)} {previousGrants && (newSinceUpgrade.size > 0 || droppedSinceUpgrade.length > 0) && (
Capability changes since {previousVersion ?? 'the previous version'}
{newSinceUpgrade.size > 0 && (
New:{' '} {[...newSinceUpgrade].map((c) => ( {c} ))}
)} {droppedSinceUpgrade.length > 0 && (
Dropped:{' '} {droppedSinceUpgrade.map((c) => ( {c} ))}
)}
)}
{tab === 'source' ? ( ) : (
    {rows.length === 0 && (
  • This extension requests no capabilities — viewer-only chrome.
  • )} {rows.map((row) => (
  • toggle(row.raw, e.target.checked)} aria-label={`Grant capability ${row.raw}`} />
    {row.raw}

    {row.risk?.description ?? 'Unknown capability — treated as high-risk.'}

  • ))}
)} {needsConfirm && tab === 'capabilities' && (
High-risk capability requested

Type {APPROVE_PHRASE} below to confirm.

setConfirmText(e.target.value)} placeholder="approve" aria-label="Type approve to confirm" autoFocus />
)}
); } function RiskIcon({ tier }: { tier: RiskTier }) { if (tier === 'red') return ; if (tier === 'yellow') return ; return ; } function RiskBadge({ tier }: { tier: RiskTier }) { return ( {tier} ); }