@burdenoff/microfe-vibecontrols 2026.531.13 → 2026.531.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -105,10 +105,10 @@ var x = ({ vibeId: x, agentId: w, profile: T, tunnelUrl: E, tr: D }) => {
105
105
  return;
106
106
  }
107
107
  try {
108
- let a = await r(w, T, "/ungit");
108
+ let a = await r(w, T, "/git");
109
109
  if (e) return;
110
- let o = `${E.replace(/\/$/, "")}/ungit/`;
111
- Q(n(o, a.token)), t = i(w, T, "/ungit", a, (e) => {
110
+ let o = `${E.replace(/\/$/, "")}/git/`;
111
+ Q(n(o, a.token)), t = i(w, T, "/git", a, (e) => {
112
112
  Q(n(o, e.token));
113
113
  });
114
114
  } catch (t) {
@@ -1 +1 @@
1
- {"version":3,"file":"VibeGitOpsTab.js","names":[],"sources":["../../../src/components/gitops/VibeGitOpsTab.tsx"],"sourcesContent":["/**\n * VibeGitOpsTab — GitOps tab content for the Vibe detail page.\n *\n * V2: All data is sourced from the agent over the tunnel via\n * `/api/profiles/<profile>/gitops/<provider>/*`. No backend GraphQL\n * involvement. The backend's old `gitopsProvider` records are deprecated.\n *\n * Sub-tabs:\n * 1. Repo Stats — repo metadata + PR + issue + security counts\n * 2. CI Pipelines — recent runs + analytics percentiles\n * 3. Git UI — Ungit iframe (when vibe-plugin-tool-git is installed)\n */\nimport { useCallback, useEffect, useMemo, useState, type FC } from 'react';\nimport { motion } from 'framer-motion';\nimport { GitMerge, BarChart3, Play, GitBranch, RefreshCw } from 'lucide-react';\nimport { useQueryClient } from '@tanstack/react-query';\n\nimport { RepoStatsTab } from './RepoStatsTab';\nimport { CITab } from './CITab';\nimport { GitOpsSetup } from './GitOpsSetup';\nimport { GitOpsProviderPicker } from './GitOpsProviderPicker';\nimport { PluginGate } from '@/components/shared';\nimport { GIT_PACKAGE } from '@/constants/pluginCatalog';\nimport { useVibeControls } from '@/providers/VibeControlsProvider';\nimport {\n useGitopsContributors,\n useGitopsIssues,\n useGitopsLanguages,\n useGitopsPipelineAnalytics,\n useGitopsProviders,\n useGitopsPullRequests,\n useGitopsRepos,\n useGitopsRuns,\n useGitopsSecurityAlerts,\n} from '@/hooks/useGitops';\nimport { composeCiStatus, composeRepoStats, type ProviderName, type RepoFqn } from './types';\nimport {\n mintIframeToken,\n scheduleIframeTokenRefresh,\n appendIframeTokenQuery,\n type IframeTokenAutoRefreshHandle,\n type IframeTokenResponse,\n} from '@/services/iframeToken';\n\ninterface VibeGitOpsTabProps {\n vibeId: string;\n agentId: string;\n /** Canonical agent profile name. */\n profile: string;\n /**\n * Tunnel URL — retained ONLY for the `git-ui` iframe handshake\n * (ungit subpath) which still bootstraps via a one-time iframe token.\n * GitOps REST calls no longer use this; they route through the svc\n * proxy (F4 Wave C).\n */\n tunnelUrl: string | null;\n tr: (key: string, fallback: string) => string;\n}\n\ntype GitOpsSubTab = 'repo-stats' | 'ci' | 'git-ui';\n\nexport const VibeGitOpsTab: FC<VibeGitOpsTabProps> = ({\n vibeId,\n agentId,\n profile,\n tunnelUrl,\n tr,\n}) => {\n const [subTab, setSubTab] = useState<GitOpsSubTab>('repo-stats');\n const [showSetup, setShowSetup] = useState(false);\n const [selectedProvider, setSelectedProvider] = useState<ProviderName | null>(null);\n const [selectedRepoFqn, setSelectedRepoFqn] = useState<RepoFqn | null>(null);\n const { workspaceId } = useVibeControls();\n const qc = useQueryClient();\n\n const ctx = useMemo(\n () => ({ profile, workspaceId: workspaceId ?? '', agentId }),\n [profile, workspaceId, agentId]\n );\n\n // Providers registered on the agent.\n const providersQ = useGitopsProviders(ctx);\n const authedProviders = useMemo(\n () => providersQ.data?.filter((p) => p.registered && p.authenticated) ?? [],\n [providersQ.data]\n );\n const hasProviders = authedProviders.length > 0;\n\n // Pick a default once data loads.\n useEffect(() => {\n if (!selectedProvider && authedProviders.length > 0) {\n const first = authedProviders[0];\n if (first) {\n setSelectedProvider(first.name as ProviderName);\n }\n }\n }, [authedProviders, selectedProvider]);\n\n // Once a provider is selected, fetch repos for it and pick the first one\n // by default. The user can switch repos via the picker.\n const reposQ = useGitopsRepos(ctx, selectedProvider, { limit: 30 });\n useEffect(() => {\n if (!selectedRepoFqn && reposQ.data?.items.length) {\n const first = reposQ.data.items[0];\n if (first) {\n setSelectedRepoFqn(first.fqn);\n }\n }\n }, [reposQ.data, selectedRepoFqn]);\n\n // Repo Stats composite — runs only when active sub-tab is repo-stats.\n const repoQ = useGitopsRepos(ctx, selectedProvider, { limit: 1 }); // metadata via list (1st item)\n const repoDetailQ = useGitopsRepos.length ? null : null; // keep TS happy; we use repoQ.items[0]\n const pullsQ = useGitopsPullRequests(ctx, selectedProvider, selectedRepoFqn, {\n state: 'open',\n limit: 100,\n });\n const issuesQ = useGitopsIssues(ctx, selectedProvider, selectedRepoFqn, {\n state: 'open',\n limit: 100,\n });\n const languagesQ = useGitopsLanguages(ctx, selectedProvider, selectedRepoFqn);\n const contributorsQ = useGitopsContributors(ctx, selectedProvider, selectedRepoFqn, 20);\n const securityQ = useGitopsSecurityAlerts(ctx, selectedProvider, selectedRepoFqn);\n\n const activeRepo = useMemo(\n () =>\n selectedRepoFqn\n ? (repoQ.data?.items.find((r) => r.fqn === selectedRepoFqn) ??\n reposQ.data?.items.find((r) => r.fqn === selectedRepoFqn) ??\n null)\n : (repoQ.data?.items[0] ?? null),\n [repoQ.data, reposQ.data, selectedRepoFqn]\n );\n\n const repoStats = useMemo(\n () =>\n composeRepoStats({\n repo: activeRepo,\n pulls: pullsQ.data,\n issues: issuesQ.data,\n languages: languagesQ.data,\n contributors: contributorsQ.data,\n securityAlerts: securityQ.data,\n }),\n [activeRepo, pullsQ.data, issuesQ.data, languagesQ.data, contributorsQ.data, securityQ.data]\n );\n\n // CI Status composite.\n const runsQ = useGitopsRuns(ctx, selectedProvider, selectedRepoFqn, { limit: 30 });\n const ciAnalyticsQ = useGitopsPipelineAnalytics(ctx, selectedProvider, selectedRepoFqn);\n const ciStatus = useMemo(\n () =>\n composeCiStatus({\n pipelines: undefined,\n runs: runsQ.data,\n analytics: ciAnalyticsQ.data,\n }),\n [runsQ.data, ciAnalyticsQ.data]\n );\n\n const repoStatsLoading = pullsQ.isLoading || issuesQ.isLoading || languagesQ.isLoading;\n const ciStatusLoading = runsQ.isLoading || ciAnalyticsQ.isLoading;\n const repoStatsError = (pullsQ.error || issuesQ.error || languagesQ.error)?.message;\n const ciStatusError = (runsQ.error || ciAnalyticsQ.error)?.message;\n\n const handleRefresh = useCallback(() => {\n void qc.invalidateQueries({ queryKey: ['gitops', workspaceId ?? '', agentId, profile] });\n }, [qc, workspaceId, agentId, profile]);\n\n const isRefreshing = pullsQ.isFetching || runsQ.isFetching;\n\n const subTabs: { id: GitOpsSubTab; label: string; icon: typeof BarChart3 }[] = [\n { id: 'repo-stats', label: tr('gitops.repoStats', 'Repo Stats'), icon: BarChart3 },\n { id: 'ci', label: tr('gitops.ci', 'CI Pipelines'), icon: Play },\n { id: 'git-ui', label: tr('gitops.gitUI', 'Git UI'), icon: GitBranch },\n ];\n\n // Ungit iframe state (unchanged from v1).\n const [ungitIframeUrl, setUngitIframeUrl] = useState<string | null>(null);\n const [ungitTokenError, setUngitTokenError] = useState<string | null>(null);\n useEffect(() => {\n let cancelled = false;\n let handle: IframeTokenAutoRefreshHandle | null = null;\n const setup = async () => {\n if (!agentId || !tunnelUrl || subTab !== 'git-ui') {\n setUngitIframeUrl(null);\n return;\n }\n try {\n // F5 residual — Ungit's reverse proxy lives at `/ungit/` (declared\n // by the external `vibe-plugin-tool-git` package's publicPaths +\n // meta.url override). The plugin's createUngitProxy validates the\n // entry-document auth via `?vt=<token>` (or `?apiKey=`), then\n // issues its own session cookie for sub-requests. Switching this\n // to a host-level hash→cookie shim (like F5 does for `/ui/<p>`)\n // would require an SDK contract change + coordinated republish of\n // the external plugin package. Until that ships, the iframe-token\n // sits in the URL once. Mitigations in place: F6 makes it single-\n // use (replayed token rejected); F7 strips the token before\n // upstream forwarding so Ungit never sees it; F10 enforces Origin\n // on non-GET/HEAD sub-requests. Residual surface: one query string\n // appears in CF logs + browser history for the initial document.\n const initial: IframeTokenResponse = await mintIframeToken(agentId, profile, '/ungit');\n if (cancelled) return;\n const baseUrl = `${tunnelUrl.replace(/\\/$/, '')}/ungit/`;\n setUngitIframeUrl(appendIframeTokenQuery(baseUrl, initial.token));\n handle = scheduleIframeTokenRefresh(\n agentId,\n profile,\n '/ungit',\n initial,\n (next: IframeTokenResponse) => {\n setUngitIframeUrl(appendIframeTokenQuery(baseUrl, next.token));\n }\n );\n } catch (err) {\n if (!cancelled) {\n setUngitTokenError(err instanceof Error ? err.message : 'Failed to fetch iframe token');\n }\n }\n };\n void setup();\n return () => {\n cancelled = true;\n handle?.cancel();\n };\n }, [agentId, profile, tunnelUrl, subTab]);\n\n return (\n <motion.div\n key=\"gitops\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-4\"\n data-vibe-id={vibeId}\n >\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <GitMerge className=\"size-5 text-text-secondary\" />\n <h3 className=\"font-semibold text-text-primary\">\n {tr('vibeDetails.gitopsTitle', 'GitOps')}\n </h3>\n </div>\n <div className=\"flex items-center gap-2\">\n {hasProviders && (\n <GitOpsProviderPicker\n providers={authedProviders.map((p) => p.name as ProviderName)}\n selected={selectedProvider}\n onChange={setSelectedProvider}\n />\n )}\n {hasProviders && (subTab === 'repo-stats' || subTab === 'ci') && (\n <button\n type=\"button\"\n onClick={handleRefresh}\n disabled={isRefreshing}\n className=\"flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md border border-border-default text-text-secondary hover:text-text-primary hover:border-border-strong disabled:opacity-50 transition-colors\"\n data-testid=\"gitops-refresh\"\n >\n <RefreshCw className={`size-3 ${isRefreshing ? 'animate-spin' : ''}`} />\n {tr('gitops.refresh', 'Refresh')}\n </button>\n )}\n {!showSetup && (\n <button\n type=\"button\"\n onClick={() => setShowSetup(true)}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n data-testid=\"gitops-add-provider\"\n >\n {hasProviders\n ? tr('gitops.addAnotherProvider', 'Add Provider')\n : tr('gitops.addProvider', 'Add Provider')}\n </button>\n )}\n </div>\n </div>\n\n <div className=\"flex flex-col gap-4 lg:flex-row lg:gap-6\">\n <nav\n className=\"w-full flex-shrink-0 rounded-lg border border-border-default bg-bg-surface p-2 lg:w-56\"\n role=\"tablist\"\n aria-label={tr('gitops.navigation', 'GitOps navigation')}\n >\n {subTabs.map((tab) => {\n const Icon = tab.icon;\n const isActive = subTab === tab.id;\n return (\n <button\n type=\"button\"\n key={tab.id}\n role=\"tab\"\n aria-selected={isActive}\n onClick={() => setSubTab(tab.id)}\n className={`flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm font-medium transition-colors ${\n isActive\n ? 'bg-action-primary-bg text-action-primary-text shadow-sm'\n : 'text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary'\n }`}\n >\n <Icon className=\"size-4 shrink-0\" />\n <span className=\"truncate\">{tab.label}</span>\n </button>\n );\n })}\n </nav>\n\n <div className=\"min-w-0 flex-1 space-y-4\">\n {showSetup && (\n <GitOpsSetup\n ctx={ctx}\n onCreated={() => {\n setShowSetup(false);\n void providersQ.refetch();\n }}\n onCancel={() => setShowSetup(false)}\n />\n )}\n\n {subTab === 'repo-stats' && !showSetup && (\n <>\n {!hasProviders ? (\n <EmptyState\n tr={tr}\n icon={GitMerge}\n message=\"gitops.noProvidersVibe\"\n fallback=\"No GitOps provider configured. Add a PAT to see repository stats.\"\n ctaLabel={tr('gitops.addProvider', 'Add Provider')}\n onCta={() => setShowSetup(true)}\n />\n ) : !agentId ? (\n <Notice\n tr={tr}\n message=\"gitops.noTunnel\"\n fallback=\"Waiting for agent tunnel. Make sure the agent is running and a tunnel is active.\"\n />\n ) : (\n <RepoStatsTab\n stats={repoStats}\n loading={repoStatsLoading || providersQ.isLoading}\n error={repoStatsError}\n />\n )}\n </>\n )}\n\n {subTab === 'ci' && !showSetup && (\n <>\n {!hasProviders ? (\n <EmptyState\n tr={tr}\n icon={Play}\n message=\"gitops.noProvidersCi\"\n fallback=\"No GitOps provider configured. Add a PAT to see CI pipeline status.\"\n ctaLabel={tr('gitops.addProvider', 'Add Provider')}\n onCta={() => setShowSetup(true)}\n />\n ) : !agentId ? (\n <Notice tr={tr} message=\"gitops.noTunnelCi\" fallback=\"Waiting for agent tunnel.\" />\n ) : (\n <CITab\n status={ciStatus}\n loading={ciStatusLoading || providersQ.isLoading}\n error={ciStatusError}\n />\n )}\n </>\n )}\n\n {subTab === 'git-ui' && (\n <PluginGate\n agentId={agentId || null}\n required={[GIT_PACKAGE]}\n mode=\"all\"\n eyebrow={tr('gitops.ungitEyebrow', 'Ungit plugin required')}\n title={tr('gitops.ungitTitle', 'Install the visual git client')}\n description={tr(\n 'gitops.ungitDescription',\n 'The Git UI embeds the Ungit visual git client served by your agent. Install the plugin to continue.'\n )}\n >\n {ungitTokenError ? (\n <div className=\"rounded-lg border border-border-default bg-status-error-bg-subtle p-4\">\n <p className=\"text-sm text-status-error-text\">{ungitTokenError}</p>\n </div>\n ) : ungitIframeUrl ? (\n <iframe\n src={ungitIframeUrl}\n title=\"Ungit\"\n className=\"w-full min-h-[600px] rounded-lg border border-border-default\"\n />\n ) : (\n <div className=\"rounded-lg border border-border-default bg-bg-sunken p-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr('gitops.ungitLoading', 'Loading Ungit…')}\n </p>\n </div>\n )}\n </PluginGate>\n )}\n </div>\n </div>\n </motion.div>\n );\n};\n\ninterface EmptyStateProps {\n tr: (key: string, fallback: string) => string;\n icon: typeof BarChart3;\n message: string;\n fallback: string;\n ctaLabel?: string;\n onCta?: () => void;\n}\n\nconst EmptyState: FC<EmptyStateProps> = ({\n tr,\n icon: Icon,\n message,\n fallback,\n ctaLabel,\n onCta,\n}) => (\n <div className=\"rounded-lg border border-border-default bg-bg-surface p-6 text-center\">\n <Icon className=\"size-8 text-text-secondary mx-auto mb-3\" />\n <p className=\"text-sm text-text-secondary\">{tr(message, fallback)}</p>\n {ctaLabel && onCta && (\n <button\n type=\"button\"\n onClick={onCta}\n className=\"mt-3 px-4 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n >\n {ctaLabel}\n </button>\n )}\n </div>\n);\n\ninterface NoticeProps {\n tr: (key: string, fallback: string) => string;\n message: string;\n fallback: string;\n}\n\nconst Notice: FC<NoticeProps> = ({ tr, message, fallback }) => (\n <div className=\"rounded-lg border border-border-default bg-bg-sunken p-4\">\n <p className=\"text-sm text-text-secondary\">{tr(message, fallback)}</p>\n </div>\n);\n"],"mappings":"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"}
1
+ {"version":3,"file":"VibeGitOpsTab.js","names":[],"sources":["../../../src/components/gitops/VibeGitOpsTab.tsx"],"sourcesContent":["/**\n * VibeGitOpsTab — GitOps tab content for the Vibe detail page.\n *\n * V2: All data is sourced from the agent over the tunnel via\n * `/api/profiles/<profile>/gitops/<provider>/*`. No backend GraphQL\n * involvement. The backend's old `gitopsProvider` records are deprecated.\n *\n * Sub-tabs:\n * 1. Repo Stats — repo metadata + PR + issue + security counts\n * 2. CI Pipelines — recent runs + analytics percentiles\n * 3. Git UI — Ungit iframe (when vibe-plugin-tool-git is installed)\n */\nimport { useCallback, useEffect, useMemo, useState, type FC } from 'react';\nimport { motion } from 'framer-motion';\nimport { GitMerge, BarChart3, Play, GitBranch, RefreshCw } from 'lucide-react';\nimport { useQueryClient } from '@tanstack/react-query';\n\nimport { RepoStatsTab } from './RepoStatsTab';\nimport { CITab } from './CITab';\nimport { GitOpsSetup } from './GitOpsSetup';\nimport { GitOpsProviderPicker } from './GitOpsProviderPicker';\nimport { PluginGate } from '@/components/shared';\nimport { GIT_PACKAGE } from '@/constants/pluginCatalog';\nimport { useVibeControls } from '@/providers/VibeControlsProvider';\nimport {\n useGitopsContributors,\n useGitopsIssues,\n useGitopsLanguages,\n useGitopsPipelineAnalytics,\n useGitopsProviders,\n useGitopsPullRequests,\n useGitopsRepos,\n useGitopsRuns,\n useGitopsSecurityAlerts,\n} from '@/hooks/useGitops';\nimport { composeCiStatus, composeRepoStats, type ProviderName, type RepoFqn } from './types';\nimport {\n mintIframeToken,\n scheduleIframeTokenRefresh,\n appendIframeTokenQuery,\n type IframeTokenAutoRefreshHandle,\n type IframeTokenResponse,\n} from '@/services/iframeToken';\n\ninterface VibeGitOpsTabProps {\n vibeId: string;\n agentId: string;\n /** Canonical agent profile name. */\n profile: string;\n /**\n * Tunnel URL — retained ONLY for the `git-ui` iframe handshake\n * (Ungit reverse-proxied at `/git/`) which still bootstraps via a\n * one-time iframe token. GitOps REST calls no longer use this; they\n * route through the svc proxy (F4 Wave C).\n */\n tunnelUrl: string | null;\n tr: (key: string, fallback: string) => string;\n}\n\ntype GitOpsSubTab = 'repo-stats' | 'ci' | 'git-ui';\n\nexport const VibeGitOpsTab: FC<VibeGitOpsTabProps> = ({\n vibeId,\n agentId,\n profile,\n tunnelUrl,\n tr,\n}) => {\n const [subTab, setSubTab] = useState<GitOpsSubTab>('repo-stats');\n const [showSetup, setShowSetup] = useState(false);\n const [selectedProvider, setSelectedProvider] = useState<ProviderName | null>(null);\n const [selectedRepoFqn, setSelectedRepoFqn] = useState<RepoFqn | null>(null);\n const { workspaceId } = useVibeControls();\n const qc = useQueryClient();\n\n const ctx = useMemo(\n () => ({ profile, workspaceId: workspaceId ?? '', agentId }),\n [profile, workspaceId, agentId]\n );\n\n // Providers registered on the agent.\n const providersQ = useGitopsProviders(ctx);\n const authedProviders = useMemo(\n () => providersQ.data?.filter((p) => p.registered && p.authenticated) ?? [],\n [providersQ.data]\n );\n const hasProviders = authedProviders.length > 0;\n\n // Pick a default once data loads.\n useEffect(() => {\n if (!selectedProvider && authedProviders.length > 0) {\n const first = authedProviders[0];\n if (first) {\n setSelectedProvider(first.name as ProviderName);\n }\n }\n }, [authedProviders, selectedProvider]);\n\n // Once a provider is selected, fetch repos for it and pick the first one\n // by default. The user can switch repos via the picker.\n const reposQ = useGitopsRepos(ctx, selectedProvider, { limit: 30 });\n useEffect(() => {\n if (!selectedRepoFqn && reposQ.data?.items.length) {\n const first = reposQ.data.items[0];\n if (first) {\n setSelectedRepoFqn(first.fqn);\n }\n }\n }, [reposQ.data, selectedRepoFqn]);\n\n // Repo Stats composite — runs only when active sub-tab is repo-stats.\n const repoQ = useGitopsRepos(ctx, selectedProvider, { limit: 1 }); // metadata via list (1st item)\n const repoDetailQ = useGitopsRepos.length ? null : null; // keep TS happy; we use repoQ.items[0]\n const pullsQ = useGitopsPullRequests(ctx, selectedProvider, selectedRepoFqn, {\n state: 'open',\n limit: 100,\n });\n const issuesQ = useGitopsIssues(ctx, selectedProvider, selectedRepoFqn, {\n state: 'open',\n limit: 100,\n });\n const languagesQ = useGitopsLanguages(ctx, selectedProvider, selectedRepoFqn);\n const contributorsQ = useGitopsContributors(ctx, selectedProvider, selectedRepoFqn, 20);\n const securityQ = useGitopsSecurityAlerts(ctx, selectedProvider, selectedRepoFqn);\n\n const activeRepo = useMemo(\n () =>\n selectedRepoFqn\n ? (repoQ.data?.items.find((r) => r.fqn === selectedRepoFqn) ??\n reposQ.data?.items.find((r) => r.fqn === selectedRepoFqn) ??\n null)\n : (repoQ.data?.items[0] ?? null),\n [repoQ.data, reposQ.data, selectedRepoFqn]\n );\n\n const repoStats = useMemo(\n () =>\n composeRepoStats({\n repo: activeRepo,\n pulls: pullsQ.data,\n issues: issuesQ.data,\n languages: languagesQ.data,\n contributors: contributorsQ.data,\n securityAlerts: securityQ.data,\n }),\n [activeRepo, pullsQ.data, issuesQ.data, languagesQ.data, contributorsQ.data, securityQ.data]\n );\n\n // CI Status composite.\n const runsQ = useGitopsRuns(ctx, selectedProvider, selectedRepoFqn, { limit: 30 });\n const ciAnalyticsQ = useGitopsPipelineAnalytics(ctx, selectedProvider, selectedRepoFqn);\n const ciStatus = useMemo(\n () =>\n composeCiStatus({\n pipelines: undefined,\n runs: runsQ.data,\n analytics: ciAnalyticsQ.data,\n }),\n [runsQ.data, ciAnalyticsQ.data]\n );\n\n const repoStatsLoading = pullsQ.isLoading || issuesQ.isLoading || languagesQ.isLoading;\n const ciStatusLoading = runsQ.isLoading || ciAnalyticsQ.isLoading;\n const repoStatsError = (pullsQ.error || issuesQ.error || languagesQ.error)?.message;\n const ciStatusError = (runsQ.error || ciAnalyticsQ.error)?.message;\n\n const handleRefresh = useCallback(() => {\n void qc.invalidateQueries({ queryKey: ['gitops', workspaceId ?? '', agentId, profile] });\n }, [qc, workspaceId, agentId, profile]);\n\n const isRefreshing = pullsQ.isFetching || runsQ.isFetching;\n\n const subTabs: { id: GitOpsSubTab; label: string; icon: typeof BarChart3 }[] = [\n { id: 'repo-stats', label: tr('gitops.repoStats', 'Repo Stats'), icon: BarChart3 },\n { id: 'ci', label: tr('gitops.ci', 'CI Pipelines'), icon: Play },\n { id: 'git-ui', label: tr('gitops.gitUI', 'Git UI'), icon: GitBranch },\n ];\n\n // Ungit iframe state (unchanged from v1).\n const [ungitIframeUrl, setUngitIframeUrl] = useState<string | null>(null);\n const [ungitTokenError, setUngitTokenError] = useState<string | null>(null);\n useEffect(() => {\n let cancelled = false;\n let handle: IframeTokenAutoRefreshHandle | null = null;\n const setup = async () => {\n if (!agentId || !tunnelUrl || subTab !== 'git-ui') {\n setUngitIframeUrl(null);\n return;\n }\n try {\n // audit-B P0-SEC-03 residual closure — Ungit's reverse proxy\n // now lives at `/git/` (was `/ungit/`). `/git` joins\n // `/terminal`, `/ui`, `/plan`, `/code-server` as a first-class\n // iframe-token surface in the agent's `isValidIframePathPrefix`\n // allow-list, so the microfe can mint a scoped single-use\n // token and pass it once as `?vt=` for the entry document. The\n // plugin proxy verifies the ticket via `iframeBridge`, issues\n // its own `__vibe_ungit_session` cookie, and strips the token\n // from upstream forwards. Sub-requests use the cookie + Origin\n // CSRF guard. Mitigations: F6 single-use; F7 upstream strip;\n // F10 Origin on non-GET/HEAD.\n const initial: IframeTokenResponse = await mintIframeToken(agentId, profile, '/git');\n if (cancelled) return;\n const baseUrl = `${tunnelUrl.replace(/\\/$/, '')}/git/`;\n setUngitIframeUrl(appendIframeTokenQuery(baseUrl, initial.token));\n handle = scheduleIframeTokenRefresh(\n agentId,\n profile,\n '/git',\n initial,\n (next: IframeTokenResponse) => {\n setUngitIframeUrl(appendIframeTokenQuery(baseUrl, next.token));\n }\n );\n } catch (err) {\n if (!cancelled) {\n setUngitTokenError(err instanceof Error ? err.message : 'Failed to fetch iframe token');\n }\n }\n };\n void setup();\n return () => {\n cancelled = true;\n handle?.cancel();\n };\n }, [agentId, profile, tunnelUrl, subTab]);\n\n return (\n <motion.div\n key=\"gitops\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-4\"\n data-vibe-id={vibeId}\n >\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <GitMerge className=\"size-5 text-text-secondary\" />\n <h3 className=\"font-semibold text-text-primary\">\n {tr('vibeDetails.gitopsTitle', 'GitOps')}\n </h3>\n </div>\n <div className=\"flex items-center gap-2\">\n {hasProviders && (\n <GitOpsProviderPicker\n providers={authedProviders.map((p) => p.name as ProviderName)}\n selected={selectedProvider}\n onChange={setSelectedProvider}\n />\n )}\n {hasProviders && (subTab === 'repo-stats' || subTab === 'ci') && (\n <button\n type=\"button\"\n onClick={handleRefresh}\n disabled={isRefreshing}\n className=\"flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md border border-border-default text-text-secondary hover:text-text-primary hover:border-border-strong disabled:opacity-50 transition-colors\"\n data-testid=\"gitops-refresh\"\n >\n <RefreshCw className={`size-3 ${isRefreshing ? 'animate-spin' : ''}`} />\n {tr('gitops.refresh', 'Refresh')}\n </button>\n )}\n {!showSetup && (\n <button\n type=\"button\"\n onClick={() => setShowSetup(true)}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n data-testid=\"gitops-add-provider\"\n >\n {hasProviders\n ? tr('gitops.addAnotherProvider', 'Add Provider')\n : tr('gitops.addProvider', 'Add Provider')}\n </button>\n )}\n </div>\n </div>\n\n <div className=\"flex flex-col gap-4 lg:flex-row lg:gap-6\">\n <nav\n className=\"w-full flex-shrink-0 rounded-lg border border-border-default bg-bg-surface p-2 lg:w-56\"\n role=\"tablist\"\n aria-label={tr('gitops.navigation', 'GitOps navigation')}\n >\n {subTabs.map((tab) => {\n const Icon = tab.icon;\n const isActive = subTab === tab.id;\n return (\n <button\n type=\"button\"\n key={tab.id}\n role=\"tab\"\n aria-selected={isActive}\n onClick={() => setSubTab(tab.id)}\n className={`flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm font-medium transition-colors ${\n isActive\n ? 'bg-action-primary-bg text-action-primary-text shadow-sm'\n : 'text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary'\n }`}\n >\n <Icon className=\"size-4 shrink-0\" />\n <span className=\"truncate\">{tab.label}</span>\n </button>\n );\n })}\n </nav>\n\n <div className=\"min-w-0 flex-1 space-y-4\">\n {showSetup && (\n <GitOpsSetup\n ctx={ctx}\n onCreated={() => {\n setShowSetup(false);\n void providersQ.refetch();\n }}\n onCancel={() => setShowSetup(false)}\n />\n )}\n\n {subTab === 'repo-stats' && !showSetup && (\n <>\n {!hasProviders ? (\n <EmptyState\n tr={tr}\n icon={GitMerge}\n message=\"gitops.noProvidersVibe\"\n fallback=\"No GitOps provider configured. Add a PAT to see repository stats.\"\n ctaLabel={tr('gitops.addProvider', 'Add Provider')}\n onCta={() => setShowSetup(true)}\n />\n ) : !agentId ? (\n <Notice\n tr={tr}\n message=\"gitops.noTunnel\"\n fallback=\"Waiting for agent tunnel. Make sure the agent is running and a tunnel is active.\"\n />\n ) : (\n <RepoStatsTab\n stats={repoStats}\n loading={repoStatsLoading || providersQ.isLoading}\n error={repoStatsError}\n />\n )}\n </>\n )}\n\n {subTab === 'ci' && !showSetup && (\n <>\n {!hasProviders ? (\n <EmptyState\n tr={tr}\n icon={Play}\n message=\"gitops.noProvidersCi\"\n fallback=\"No GitOps provider configured. Add a PAT to see CI pipeline status.\"\n ctaLabel={tr('gitops.addProvider', 'Add Provider')}\n onCta={() => setShowSetup(true)}\n />\n ) : !agentId ? (\n <Notice tr={tr} message=\"gitops.noTunnelCi\" fallback=\"Waiting for agent tunnel.\" />\n ) : (\n <CITab\n status={ciStatus}\n loading={ciStatusLoading || providersQ.isLoading}\n error={ciStatusError}\n />\n )}\n </>\n )}\n\n {subTab === 'git-ui' && (\n <PluginGate\n agentId={agentId || null}\n required={[GIT_PACKAGE]}\n mode=\"all\"\n eyebrow={tr('gitops.ungitEyebrow', 'Ungit plugin required')}\n title={tr('gitops.ungitTitle', 'Install the visual git client')}\n description={tr(\n 'gitops.ungitDescription',\n 'The Git UI embeds the Ungit visual git client served by your agent. Install the plugin to continue.'\n )}\n >\n {ungitTokenError ? (\n <div className=\"rounded-lg border border-border-default bg-status-error-bg-subtle p-4\">\n <p className=\"text-sm text-status-error-text\">{ungitTokenError}</p>\n </div>\n ) : ungitIframeUrl ? (\n <iframe\n src={ungitIframeUrl}\n title=\"Ungit\"\n className=\"w-full min-h-[600px] rounded-lg border border-border-default\"\n />\n ) : (\n <div className=\"rounded-lg border border-border-default bg-bg-sunken p-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr('gitops.ungitLoading', 'Loading Ungit…')}\n </p>\n </div>\n )}\n </PluginGate>\n )}\n </div>\n </div>\n </motion.div>\n );\n};\n\ninterface EmptyStateProps {\n tr: (key: string, fallback: string) => string;\n icon: typeof BarChart3;\n message: string;\n fallback: string;\n ctaLabel?: string;\n onCta?: () => void;\n}\n\nconst EmptyState: FC<EmptyStateProps> = ({\n tr,\n icon: Icon,\n message,\n fallback,\n ctaLabel,\n onCta,\n}) => (\n <div className=\"rounded-lg border border-border-default bg-bg-surface p-6 text-center\">\n <Icon className=\"size-8 text-text-secondary mx-auto mb-3\" />\n <p className=\"text-sm text-text-secondary\">{tr(message, fallback)}</p>\n {ctaLabel && onCta && (\n <button\n type=\"button\"\n onClick={onCta}\n className=\"mt-3 px-4 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n >\n {ctaLabel}\n </button>\n )}\n </div>\n);\n\ninterface NoticeProps {\n tr: (key: string, fallback: string) => string;\n message: string;\n fallback: string;\n}\n\nconst Notice: FC<NoticeProps> = ({ tr, message, fallback }) => (\n <div className=\"rounded-lg border border-border-default bg-bg-sunken p-4\">\n <p className=\"text-sm text-text-secondary\">{tr(message, fallback)}</p>\n </div>\n);\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { agentAiMutate as e } from "./aiApi.js";
2
2
  import { agentApiPath as t } from "../utils/agentApiPath.js";
3
- var n = 600, r = 6e4, i = /^\/terminal\/[A-Za-z0-9_-]{1,128}(?:\/.*)?$/, a = /^\/ui\/[a-z0-9-]{1,64}(?:\/.*)?$/, o = /^\/plan\/[A-Za-z0-9_-]{1,128}(?:\/.*)?$/, s = /^\/code-server(?:\/.*)?$/, c = /^\/api\/plugins\/[a-z0-9-]{1,64}\/ui(?:\/.*)?$/, l = new Set([
3
+ var n = 600, r = 6e4, i = /^\/terminal\/[A-Za-z0-9_-]{1,128}(?:\/.*)?$/, a = /^\/ui\/[a-z0-9-]{1,64}(?:\/.*)?$/, o = /^\/plan\/[A-Za-z0-9_-]{1,128}(?:\/.*)?$/, s = /^\/git(?:\/.*)?$/, c = /^\/code-server(?:\/.*)?$/, l = /^\/api\/plugins\/[a-z0-9-]{1,64}\/ui(?:\/.*)?$/, u = new Set([
4
4
  "/",
5
5
  "/api",
6
6
  "/api/agent",
@@ -12,44 +12,44 @@ var n = 600, r = 6e4, i = /^\/terminal\/[A-Za-z0-9_-]{1,128}(?:\/.*)?$/, a = /^\
12
12
  "/api/ai",
13
13
  "/api/logs"
14
14
  ]);
15
- function u(e) {
16
- return typeof e != "string" || e.length === 0 || e.includes("\0") || e.includes("\n") || e.includes("|") || e.includes("..") || l.has(e) ? !1 : !!(i.test(e) || a.test(e) || o.test(e) || e === "/ws/logs" || s.test(e) || c.test(e));
15
+ function d(e) {
16
+ return typeof e != "string" || e.length === 0 || e.includes("\0") || e.includes("\n") || e.includes("|") || e.includes("..") || u.has(e) ? !1 : !!(i.test(e) || a.test(e) || o.test(e) || s.test(e) || e === "/ws/logs" || c.test(e) || l.test(e));
17
17
  }
18
- function d(e, t) {
18
+ function f(e, t) {
19
19
  let n = [];
20
- for (let t of e) u(t) || n.push(t);
20
+ for (let t of e) d(t) || n.push(t);
21
21
  if (n.length > 0) throw Error(`iframe-token mint blocked: ${n.length} prefix(es) outside agent allow-list (${n.join(", ")}) for scope=${t}. Migrate the iframe REST surface to /api/plugins/<plugin>/ui or /ui/<plugin>.`);
22
22
  }
23
- async function f(r, i, a, o = n) {
23
+ async function p(r, i, a, o = n) {
24
24
  let s = Math.max(1, Math.min(600, Math.floor(o)));
25
- return d([a], "mintIframeToken"), e(r, t(i, "/agent/iframe-token"), "POST", {
25
+ return f([a], "mintIframeToken"), e(r, t(i, "/agent/iframe-token"), "POST", {
26
26
  pathPrefix: a,
27
27
  ttlSeconds: s
28
28
  });
29
29
  }
30
- async function p(r, i, a, o = n) {
30
+ async function m(r, i, a, o = n) {
31
31
  let s = Math.max(1, Math.min(600, Math.floor(o)));
32
- return d(a, "mintScopedIframeToken"), e(r, t(i, "/agent/iframe-token"), "POST", {
32
+ return f(a, "mintScopedIframeToken"), e(r, t(i, "/agent/iframe-token"), "POST", {
33
33
  pathPrefixes: a,
34
34
  ttlSeconds: s
35
35
  });
36
36
  }
37
- function m(e, t, i, a, o, s, c = n) {
38
- let l = !1, u = null, d = Date.parse(a.expiresAt), p = Math.max(1e3, d - Date.now() - r);
37
+ function h(e, t, i, a, o, s, c = n) {
38
+ let l = !1, u = null, d = Date.parse(a.expiresAt), f = Math.max(1e3, d - Date.now() - r);
39
39
  return u = setTimeout(() => {
40
- l || f(e, t, i, c).then((e) => {
40
+ l || p(e, t, i, c).then((e) => {
41
41
  l || o(e);
42
42
  }).catch((e) => {
43
43
  l || s?.(e instanceof Error ? e : Error(String(e)));
44
44
  });
45
- }, p), { cancel: () => {
45
+ }, f), { cancel: () => {
46
46
  l = !0, u !== null && (clearTimeout(u), u = null);
47
47
  } };
48
48
  }
49
- function h(e, t, i, a, o, s, c = n) {
49
+ function g(e, t, i, a, o, s, c = n) {
50
50
  let l = !1, u = null, d = Date.parse(a.expiresAt), f = Math.max(1e3, d - Date.now() - r);
51
51
  return u = setTimeout(() => {
52
- l || p(e, t, i, c).then((e) => {
52
+ l || m(e, t, i, c).then((e) => {
53
53
  l || o(e);
54
54
  }).catch((e) => {
55
55
  l || s?.(e instanceof Error ? e : Error(String(e)));
@@ -58,15 +58,15 @@ function h(e, t, i, a, o, s, c = n) {
58
58
  l = !0, u !== null && (clearTimeout(u), u = null);
59
59
  } };
60
60
  }
61
- function g(e, t) {
61
+ function _(e, t) {
62
62
  let n = e.indexOf("#");
63
63
  return `${n === -1 ? e : e.slice(0, n)}#vt=${encodeURIComponent(t)}`;
64
64
  }
65
- function _(e, t) {
65
+ function v(e, t) {
66
66
  let n = e.indexOf("#"), r = n === -1 ? e : e.slice(0, n), i = n === -1 ? "" : e.slice(n);
67
67
  return `${r}${r.includes("?") ? "&" : "?"}vt=${encodeURIComponent(t)}${i}`;
68
68
  }
69
69
  //#endregion
70
- export { g as appendIframeTokenFragment, _ as appendIframeTokenQuery, f as mintIframeToken, p as mintScopedIframeToken, m as scheduleIframeTokenRefresh, h as scheduleScopedIframeTokenRefresh };
70
+ export { _ as appendIframeTokenFragment, v as appendIframeTokenQuery, p as mintIframeToken, m as mintScopedIframeToken, h as scheduleIframeTokenRefresh, g as scheduleScopedIframeTokenRefresh };
71
71
 
72
72
  //# sourceMappingURL=iframeToken.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"iframeToken.js","names":[],"sources":["../../src/services/iframeToken.ts"],"sourcesContent":["/**\n * iframe-token service\n *\n * Mints short-lived signed tokens for iframe embeds talking to the\n * vibecontrols-agent. Replaces the previous Referer/api-key-in-URL flow\n * dropped by the agent in Phase C.\n *\n * Backend route: POST /api/agent/iframe-token\n * Request: { pathPrefix: string, ttlSeconds?: number }\n * Response: { token: string, expiresAt: string } // ISO-8601\n *\n * Hard limit: ttlSeconds <= 600 (agent enforces; we cap client-side too).\n */\n\nimport { agentAiMutate } from './aiApi';\nimport { agentApiPath } from '@/utils/agentApiPath';\n\nexport const MAX_IFRAME_TOKEN_TTL_SECONDS = 600;\nconst DEFAULT_TTL_SECONDS = 600;\nconst REFRESH_LEEWAY_MS = 60_000;\n\n/**\n * Client-side mirror of the agent's iframe-token `pathPrefix` allow-list\n * (`vibecontrols-agent/src/core/iframe-tokens.ts::isValidIframePathPrefix`).\n * audit-B P0-SEC-03 / agent commit c916492 — anything outside this list is\n * rejected at the agent. We validate locally so the UI surfaces a clear\n * error instead of a 400 round-trip.\n */\nconst IFRAME_PREFIX_TERMINAL_RE = /^\\/terminal\\/[A-Za-z0-9_-]{1,128}(?:\\/.*)?$/;\nconst IFRAME_PREFIX_UI_RE = /^\\/ui\\/[a-z0-9-]{1,64}(?:\\/.*)?$/;\nconst IFRAME_PREFIX_PLAN_RE = /^\\/plan\\/[A-Za-z0-9_-]{1,128}(?:\\/.*)?$/;\nconst IFRAME_PREFIX_CODE_SERVER_RE = /^\\/code-server(?:\\/.*)?$/;\nconst IFRAME_PREFIX_PLUGIN_UI_RE = /^\\/api\\/plugins\\/[a-z0-9-]{1,64}\\/ui(?:\\/.*)?$/;\nconst BANNED_PREFIXES = new Set<string>([\n '/',\n '/api',\n '/api/agent',\n '/api/plugins',\n '/api/profiles',\n '/health',\n '/metrics',\n '/ws',\n '/api/ai',\n '/api/logs',\n]);\n\nexport function isAllowedAgentIframePrefix(prefix: string): boolean {\n if (typeof prefix !== 'string' || prefix.length === 0) return false;\n if (prefix.includes('\\0') || prefix.includes('\\n') || prefix.includes('|')) return false;\n if (prefix.includes('..')) return false;\n if (BANNED_PREFIXES.has(prefix)) return false;\n if (IFRAME_PREFIX_TERMINAL_RE.test(prefix)) return true;\n if (IFRAME_PREFIX_UI_RE.test(prefix)) return true;\n if (IFRAME_PREFIX_PLAN_RE.test(prefix)) return true;\n if (prefix === '/ws/logs') return true;\n if (IFRAME_PREFIX_CODE_SERVER_RE.test(prefix)) return true;\n if (IFRAME_PREFIX_PLUGIN_UI_RE.test(prefix)) return true;\n return false;\n}\n\nfunction assertAllowedPrefixes(prefixes: readonly string[], scope: string): void {\n const rejected: string[] = [];\n for (const p of prefixes) {\n if (!isAllowedAgentIframePrefix(p)) rejected.push(p);\n }\n if (rejected.length > 0) {\n // Hard-fail rather than silently passing through — the agent would\n // reject the whole mint anyway, and silently dropping prefixes\n // would let the iframe come up partially broken.\n throw new Error(\n `iframe-token mint blocked: ${rejected.length} prefix(es) outside agent allow-list ` +\n `(${rejected.join(', ')}) for scope=${scope}. Migrate the iframe REST surface to ` +\n `/api/plugins/<plugin>/ui or /ui/<plugin>.`\n );\n }\n}\n\nexport interface IframeTokenResponse {\n token: string;\n expiresAt: string;\n}\n\nexport interface IframeTokenAutoRefreshHandle {\n /** Cancels the pending refresh timer. Idempotent. */\n cancel: () => void;\n}\n\n/**\n * Mint an iframe token for the given agent + path prefix.\n *\n * @param agentId The agent's GraphQL id.\n * @param profile Profile name on the agent that owns this iframe token (canonical URL hierarchy).\n * @param pathPrefix Iframe path prefix the token should authorize (e.g. '/ungit', '/ui/graphiql').\n * @param ttlSeconds Token lifetime; clamped to (0, 600].\n */\nexport async function mintIframeToken(\n agentId: string,\n profile: string,\n pathPrefix: string,\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): Promise<IframeTokenResponse> {\n const ttl = Math.max(1, Math.min(MAX_IFRAME_TOKEN_TTL_SECONDS, Math.floor(ttlSeconds)));\n assertAllowedPrefixes([pathPrefix], 'mintIframeToken');\n return agentAiMutate<IframeTokenResponse>(\n agentId,\n agentApiPath(profile, '/agent/iframe-token'),\n 'POST',\n { pathPrefix, ttlSeconds: ttl }\n );\n}\n\n/**\n * Mint a v2 scoped iframe token bound to an allow-list of path prefixes.\n * UI plugins use this so a single token can reach both `/ui/<plugin>`\n * (where the iframe HTML lives) and the plugin's REST surface\n * (e.g. `/api/profiles/*\\/<plugin>/*`) without exposing the master apiKey.\n *\n * Agent enforces the prefix list on every request via\n * `verifyIframeToken` — calls outside the declared prefixes are rejected\n * regardless of how the iframe acquired the token.\n */\nexport async function mintScopedIframeToken(\n agentId: string,\n profile: string,\n pathPrefixes: string[],\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): Promise<IframeTokenResponse> {\n const ttl = Math.max(1, Math.min(MAX_IFRAME_TOKEN_TTL_SECONDS, Math.floor(ttlSeconds)));\n assertAllowedPrefixes(pathPrefixes, 'mintScopedIframeToken');\n return agentAiMutate<IframeTokenResponse>(\n agentId,\n agentApiPath(profile, '/agent/iframe-token'),\n 'POST',\n { pathPrefixes, ttlSeconds: ttl }\n );\n}\n\n/**\n * Schedule an auto-refresh that re-mints the token ~60s before expiry.\n * Returns a handle whose `cancel()` clears the timer.\n *\n * Caller is responsible for re-rendering / updating iframe `src` on each\n * refresh by reacting to the `onRefresh` callback's payload.\n */\nexport function scheduleIframeTokenRefresh(\n agentId: string,\n profile: string,\n pathPrefix: string,\n current: IframeTokenResponse,\n onRefresh: (next: IframeTokenResponse) => void,\n onError?: (err: Error) => void,\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): IframeTokenAutoRefreshHandle {\n let cancelled = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const expiresAtMs = Date.parse(current.expiresAt);\n const delay = Math.max(1_000, expiresAtMs - Date.now() - REFRESH_LEEWAY_MS);\n\n timer = setTimeout(() => {\n if (cancelled) return;\n mintIframeToken(agentId, profile, pathPrefix, ttlSeconds)\n .then((next) => {\n if (cancelled) return;\n onRefresh(next);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n onError?.(err instanceof Error ? err : new Error(String(err)));\n });\n }, delay);\n\n return {\n cancel: () => {\n cancelled = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n },\n };\n}\n\n/**\n * Scoped-token variant of {@link scheduleIframeTokenRefresh}. Re-mints a\n * v2 token bound to the same prefix allow-list ~60s before expiry. Used by\n * UI plugins whose iframe also calls the plugin's profile-scoped REST\n * surface, so the refreshed token keeps authorizing both `/ui/<plugin>`\n * and `/api/profiles/<profile>/<plugin>/*`.\n */\nexport function scheduleScopedIframeTokenRefresh(\n agentId: string,\n profile: string,\n pathPrefixes: string[],\n current: IframeTokenResponse,\n onRefresh: (next: IframeTokenResponse) => void,\n onError?: (err: Error) => void,\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): IframeTokenAutoRefreshHandle {\n let cancelled = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const expiresAtMs = Date.parse(current.expiresAt);\n const delay = Math.max(1_000, expiresAtMs - Date.now() - REFRESH_LEEWAY_MS);\n\n timer = setTimeout(() => {\n if (cancelled) return;\n mintScopedIframeToken(agentId, profile, pathPrefixes, ttlSeconds)\n .then((next) => {\n if (cancelled) return;\n onRefresh(next);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n onError?.(err instanceof Error ? err : new Error(String(err)));\n });\n }, delay);\n\n return {\n cancel: () => {\n cancelled = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n },\n };\n}\n\n/**\n * Append the iframe token as a `#vt=` URL fragment to the given URL\n * (F5 — security cutover, primary entry point for /ui/<plugin> embeds).\n *\n * Fragments never reach the server, never appear in proxy access logs,\n * never land in the browser's Referer header on outbound sub-requests,\n * and aren't persisted by middlebox URL loggers. The agent's bootstrap\n * shim at `/ui/<plugin>` reads the fragment client-side and POSTs it to\n * `/api/agent/ui-exchange`, which trades the single-use iframe token\n * for a path-scoped HttpOnly cookie (`vt_ui_<plugin>`). Subsequent\n * sub-resource requests under `/ui/<plugin>/` auto-attach the cookie.\n *\n * Use this for any new iframe embed. The legacy\n * `appendIframeTokenParam(url, token)` helper was removed as part of\n * the F5 residual hard-cutover — the Ungit surface at\n * `/api/plugins/<plugin>/ui` is now behind an equivalent host-level\n * cookie shim (sibling of the F5 `/ui/<plugin>` shim) that exchanges\n * the `#vt=` fragment for a path-scoped HttpOnly cookie. There are\n * no remaining `?vt=` URL-query callers.\n */\nexport function appendIframeTokenFragment(url: string, token: string): string {\n // Strip any existing fragment so we never end up with two `#`s.\n const hashIdx = url.indexOf('#');\n const base = hashIdx === -1 ? url : url.slice(0, hashIdx);\n return `${base}#vt=${encodeURIComponent(token)}`;\n}\n\n/**\n * F5 RESIDUAL — `?vt=<token>` query-string fallback for embeds whose\n * upstream reverse proxy validates the entry document via query auth and\n * issues its own session cookie (e.g. the external `vibe-plugin-tool-git`\n * Ungit proxy at `/ungit/`). Migrating these to the fragment+cookie shim\n * needs a coordinated plugin SDK contract change + republish of the\n * plugin package — out of band for the F5 cutover.\n *\n * The exposure is narrow:\n * - F6 makes every iframe token single-use (replay → 401).\n * - F7 strips the token from the proxy's upstream forwards.\n * - F10 enforces Origin/CSRF on non-GET/HEAD sub-requests.\n *\n * Do NOT use for new embeds. Prefer `appendIframeTokenFragment`.\n */\nexport function appendIframeTokenQuery(url: string, token: string): string {\n const fragmentIdx = url.indexOf('#');\n const baseAndQuery = fragmentIdx === -1 ? url : url.slice(0, fragmentIdx);\n const fragment = fragmentIdx === -1 ? '' : url.slice(fragmentIdx);\n const separator = baseAndQuery.includes('?') ? '&' : '?';\n return `${baseAndQuery}${separator}vt=${encodeURIComponent(token)}${fragment}`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"iframeToken.js","names":[],"sources":["../../src/services/iframeToken.ts"],"sourcesContent":["/**\n * iframe-token service\n *\n * Mints short-lived signed tokens for iframe embeds talking to the\n * vibecontrols-agent. Replaces the previous Referer/api-key-in-URL flow\n * dropped by the agent in Phase C.\n *\n * Backend route: POST /api/agent/iframe-token\n * Request: { pathPrefix: string, ttlSeconds?: number }\n * Response: { token: string, expiresAt: string } // ISO-8601\n *\n * Hard limit: ttlSeconds <= 600 (agent enforces; we cap client-side too).\n */\n\nimport { agentAiMutate } from './aiApi';\nimport { agentApiPath } from '@/utils/agentApiPath';\n\nexport const MAX_IFRAME_TOKEN_TTL_SECONDS = 600;\nconst DEFAULT_TTL_SECONDS = 600;\nconst REFRESH_LEEWAY_MS = 60_000;\n\n/**\n * Client-side mirror of the agent's iframe-token `pathPrefix` allow-list\n * (`vibecontrols-agent/src/core/iframe-tokens.ts::isValidIframePathPrefix`).\n * audit-B P0-SEC-03 / agent commit c916492 — anything outside this list is\n * rejected at the agent. We validate locally so the UI surfaces a clear\n * error instead of a 400 round-trip.\n */\nconst IFRAME_PREFIX_TERMINAL_RE = /^\\/terminal\\/[A-Za-z0-9_-]{1,128}(?:\\/.*)?$/;\nconst IFRAME_PREFIX_UI_RE = /^\\/ui\\/[a-z0-9-]{1,64}(?:\\/.*)?$/;\nconst IFRAME_PREFIX_PLAN_RE = /^\\/plan\\/[A-Za-z0-9_-]{1,128}(?:\\/.*)?$/;\nconst IFRAME_PREFIX_GIT_RE = /^\\/git(?:\\/.*)?$/;\nconst IFRAME_PREFIX_CODE_SERVER_RE = /^\\/code-server(?:\\/.*)?$/;\nconst IFRAME_PREFIX_PLUGIN_UI_RE = /^\\/api\\/plugins\\/[a-z0-9-]{1,64}\\/ui(?:\\/.*)?$/;\nconst BANNED_PREFIXES = new Set<string>([\n '/',\n '/api',\n '/api/agent',\n '/api/plugins',\n '/api/profiles',\n '/health',\n '/metrics',\n '/ws',\n '/api/ai',\n '/api/logs',\n]);\n\nexport function isAllowedAgentIframePrefix(prefix: string): boolean {\n if (typeof prefix !== 'string' || prefix.length === 0) return false;\n if (prefix.includes('\\0') || prefix.includes('\\n') || prefix.includes('|')) return false;\n if (prefix.includes('..')) return false;\n if (BANNED_PREFIXES.has(prefix)) return false;\n if (IFRAME_PREFIX_TERMINAL_RE.test(prefix)) return true;\n if (IFRAME_PREFIX_UI_RE.test(prefix)) return true;\n if (IFRAME_PREFIX_PLAN_RE.test(prefix)) return true;\n if (IFRAME_PREFIX_GIT_RE.test(prefix)) return true;\n if (prefix === '/ws/logs') return true;\n if (IFRAME_PREFIX_CODE_SERVER_RE.test(prefix)) return true;\n if (IFRAME_PREFIX_PLUGIN_UI_RE.test(prefix)) return true;\n return false;\n}\n\nfunction assertAllowedPrefixes(prefixes: readonly string[], scope: string): void {\n const rejected: string[] = [];\n for (const p of prefixes) {\n if (!isAllowedAgentIframePrefix(p)) rejected.push(p);\n }\n if (rejected.length > 0) {\n // Hard-fail rather than silently passing through — the agent would\n // reject the whole mint anyway, and silently dropping prefixes\n // would let the iframe come up partially broken.\n throw new Error(\n `iframe-token mint blocked: ${rejected.length} prefix(es) outside agent allow-list ` +\n `(${rejected.join(', ')}) for scope=${scope}. Migrate the iframe REST surface to ` +\n `/api/plugins/<plugin>/ui or /ui/<plugin>.`\n );\n }\n}\n\nexport interface IframeTokenResponse {\n token: string;\n expiresAt: string;\n}\n\nexport interface IframeTokenAutoRefreshHandle {\n /** Cancels the pending refresh timer. Idempotent. */\n cancel: () => void;\n}\n\n/**\n * Mint an iframe token for the given agent + path prefix.\n *\n * @param agentId The agent's GraphQL id.\n * @param profile Profile name on the agent that owns this iframe token (canonical URL hierarchy).\n * @param pathPrefix Iframe path prefix the token should authorize (e.g. '/git', '/ui/graphiql').\n * @param ttlSeconds Token lifetime; clamped to (0, 600].\n */\nexport async function mintIframeToken(\n agentId: string,\n profile: string,\n pathPrefix: string,\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): Promise<IframeTokenResponse> {\n const ttl = Math.max(1, Math.min(MAX_IFRAME_TOKEN_TTL_SECONDS, Math.floor(ttlSeconds)));\n assertAllowedPrefixes([pathPrefix], 'mintIframeToken');\n return agentAiMutate<IframeTokenResponse>(\n agentId,\n agentApiPath(profile, '/agent/iframe-token'),\n 'POST',\n { pathPrefix, ttlSeconds: ttl }\n );\n}\n\n/**\n * Mint a v2 scoped iframe token bound to an allow-list of path prefixes.\n * UI plugins use this so a single token can reach both `/ui/<plugin>`\n * (where the iframe HTML lives) and the plugin's REST surface\n * (e.g. `/api/profiles/*\\/<plugin>/*`) without exposing the master apiKey.\n *\n * Agent enforces the prefix list on every request via\n * `verifyIframeToken` — calls outside the declared prefixes are rejected\n * regardless of how the iframe acquired the token.\n */\nexport async function mintScopedIframeToken(\n agentId: string,\n profile: string,\n pathPrefixes: string[],\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): Promise<IframeTokenResponse> {\n const ttl = Math.max(1, Math.min(MAX_IFRAME_TOKEN_TTL_SECONDS, Math.floor(ttlSeconds)));\n assertAllowedPrefixes(pathPrefixes, 'mintScopedIframeToken');\n return agentAiMutate<IframeTokenResponse>(\n agentId,\n agentApiPath(profile, '/agent/iframe-token'),\n 'POST',\n { pathPrefixes, ttlSeconds: ttl }\n );\n}\n\n/**\n * Schedule an auto-refresh that re-mints the token ~60s before expiry.\n * Returns a handle whose `cancel()` clears the timer.\n *\n * Caller is responsible for re-rendering / updating iframe `src` on each\n * refresh by reacting to the `onRefresh` callback's payload.\n */\nexport function scheduleIframeTokenRefresh(\n agentId: string,\n profile: string,\n pathPrefix: string,\n current: IframeTokenResponse,\n onRefresh: (next: IframeTokenResponse) => void,\n onError?: (err: Error) => void,\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): IframeTokenAutoRefreshHandle {\n let cancelled = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const expiresAtMs = Date.parse(current.expiresAt);\n const delay = Math.max(1_000, expiresAtMs - Date.now() - REFRESH_LEEWAY_MS);\n\n timer = setTimeout(() => {\n if (cancelled) return;\n mintIframeToken(agentId, profile, pathPrefix, ttlSeconds)\n .then((next) => {\n if (cancelled) return;\n onRefresh(next);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n onError?.(err instanceof Error ? err : new Error(String(err)));\n });\n }, delay);\n\n return {\n cancel: () => {\n cancelled = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n },\n };\n}\n\n/**\n * Scoped-token variant of {@link scheduleIframeTokenRefresh}. Re-mints a\n * v2 token bound to the same prefix allow-list ~60s before expiry. Used by\n * UI plugins whose iframe also calls the plugin's profile-scoped REST\n * surface, so the refreshed token keeps authorizing both `/ui/<plugin>`\n * and `/api/profiles/<profile>/<plugin>/*`.\n */\nexport function scheduleScopedIframeTokenRefresh(\n agentId: string,\n profile: string,\n pathPrefixes: string[],\n current: IframeTokenResponse,\n onRefresh: (next: IframeTokenResponse) => void,\n onError?: (err: Error) => void,\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): IframeTokenAutoRefreshHandle {\n let cancelled = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const expiresAtMs = Date.parse(current.expiresAt);\n const delay = Math.max(1_000, expiresAtMs - Date.now() - REFRESH_LEEWAY_MS);\n\n timer = setTimeout(() => {\n if (cancelled) return;\n mintScopedIframeToken(agentId, profile, pathPrefixes, ttlSeconds)\n .then((next) => {\n if (cancelled) return;\n onRefresh(next);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n onError?.(err instanceof Error ? err : new Error(String(err)));\n });\n }, delay);\n\n return {\n cancel: () => {\n cancelled = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n },\n };\n}\n\n/**\n * Append the iframe token as a `#vt=` URL fragment to the given URL\n * (F5 — security cutover, primary entry point for /ui/<plugin> embeds).\n *\n * Fragments never reach the server, never appear in proxy access logs,\n * never land in the browser's Referer header on outbound sub-requests,\n * and aren't persisted by middlebox URL loggers. The agent's bootstrap\n * shim at `/ui/<plugin>` reads the fragment client-side and POSTs it to\n * `/api/agent/ui-exchange`, which trades the single-use iframe token\n * for a path-scoped HttpOnly cookie (`vt_ui_<plugin>`). Subsequent\n * sub-resource requests under `/ui/<plugin>/` auto-attach the cookie.\n *\n * Use this for any new iframe embed. The legacy\n * `appendIframeTokenParam(url, token)` helper was removed as part of\n * the F5 residual hard-cutover — the Ungit surface at\n * `/api/plugins/<plugin>/ui` is now behind an equivalent host-level\n * cookie shim (sibling of the F5 `/ui/<plugin>` shim) that exchanges\n * the `#vt=` fragment for a path-scoped HttpOnly cookie. There are\n * no remaining `?vt=` URL-query callers.\n */\nexport function appendIframeTokenFragment(url: string, token: string): string {\n // Strip any existing fragment so we never end up with two `#`s.\n const hashIdx = url.indexOf('#');\n const base = hashIdx === -1 ? url : url.slice(0, hashIdx);\n return `${base}#vt=${encodeURIComponent(token)}`;\n}\n\n/**\n * `?vt=<token>` query-string fallback for embeds whose upstream reverse\n * proxy validates the entry document via query auth and issues its own\n * session cookie (e.g. the external `vibe-plugin-tool-git` Ungit proxy at\n * `/git/`). Top-level iframe navigation cannot inject headers, so the\n * single-use ticket lives in the URL once; the proxy strips it before\n * forwarding upstream and immediately swaps it for a SameSite=None\n * HttpOnly session cookie.\n *\n * The exposure is narrow:\n * - F6 makes every iframe token single-use (replay → 401).\n * - F7 strips the token from the proxy's upstream forwards.\n * - F10 enforces Origin/CSRF on non-GET/HEAD sub-requests.\n *\n * Do NOT use for new embeds. Prefer `appendIframeTokenFragment`.\n */\nexport function appendIframeTokenQuery(url: string, token: string): string {\n const fragmentIdx = url.indexOf('#');\n const baseAndQuery = fragmentIdx === -1 ? url : url.slice(0, fragmentIdx);\n const fragment = fragmentIdx === -1 ? '' : url.slice(fragmentIdx);\n const separator = baseAndQuery.includes('?') ? '&' : '?';\n return `${baseAndQuery}${separator}vt=${encodeURIComponent(token)}${fragment}`;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-vibecontrols",
3
- "version": "2026.531.13",
3
+ "version": "2026.531.14",
4
4
  "description": "VibeControls microfrontend for Burdenoff products",
5
5
  "type": "module",
6
6
  "files": [