@burdenoff/microfe-vibecontrols 2026.528.11 → 2026.529.1

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.
@@ -2,7 +2,7 @@ import { useVibeControls as e } from "../../providers/VibeControlsProvider.js";
2
2
  import "../../constants/pluginCatalog.js";
3
3
  import { PluginGate as t } from "../shared/PluginGate.js";
4
4
  import "../shared/index.js";
5
- import { appendIframeTokenParam as n, mintIframeToken as r, scheduleIframeTokenRefresh as i } from "../../services/iframeToken.js";
5
+ import { appendIframeTokenFragment as n, mintIframeToken as r, scheduleIframeTokenRefresh as i } from "../../services/iframeToken.js";
6
6
  import { RepoStatsTab as a } from "./RepoStatsTab.js";
7
7
  import { CITab as o } from "./CITab.js";
8
8
  import { useGitopsContributors as ee, useGitopsIssues as te, useGitopsLanguages as ne, useGitopsPipelineAnalytics as re, useGitopsProviders as ie, useGitopsPullRequests as ae, useGitopsRepos as s, useGitopsRuns as oe, useGitopsSecurityAlerts as c } from "../../hooks/useGitops.js";
@@ -109,10 +109,10 @@ var b = ({ vibeId: b, agentId: C, profile: w, tunnelUrl: T, apiKey: E, tr: D })
109
109
  return;
110
110
  }
111
111
  try {
112
- let a = await r(C, w, "/ungit");
112
+ let a = await r(C, w, "/api/plugins/ungit/ui");
113
113
  if (e) return;
114
114
  let o = `${T.replace(/\/$/, "")}/api/plugins/ungit/ui`;
115
- Q(n(o, a.token)), t = i(C, w, "/ungit", a, (e) => {
115
+ Q(n(o, a.token)), t = i(C, w, "/api/plugins/ungit/ui", a, (e) => {
116
116
  Q(n(o, e.token));
117
117
  });
118
118
  } 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 appendIframeTokenParam,\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 tunnelUrl: string | null;\n apiKey: 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 apiKey,\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 () => ({ tunnelUrl, apiKey, profile, workspaceId: workspaceId ?? '', agentId }),\n [tunnelUrl, apiKey, 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 const initial: IframeTokenResponse = await mintIframeToken(agentId, profile, '/ungit');\n if (cancelled) return;\n const baseUrl = `${tunnelUrl.replace(/\\/$/, '')}/api/plugins/ungit/ui`;\n setUngitIframeUrl(appendIframeTokenParam(baseUrl, initial.token));\n handle = scheduleIframeTokenRefresh(\n agentId,\n profile,\n '/ungit',\n initial,\n (next: IframeTokenResponse) => {\n setUngitIframeUrl(appendIframeTokenParam(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 ) : !tunnelUrl || !apiKey ? (\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 ) : !tunnelUrl || !apiKey ? (\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 appendIframeTokenFragment,\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 tunnelUrl: string | null;\n apiKey: 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 apiKey,\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 () => ({ tunnelUrl, apiKey, profile, workspaceId: workspaceId ?? '', agentId }),\n [tunnelUrl, apiKey, 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 hard-cutover — token is now carried as a `#vt=`\n // URL fragment (not a `?vt=` query param). The agent's host-\n // level cookie-shim at `/api/plugins/<plugin>/ui` reads the\n // fragment client-side, POSTs it to `/api/agent/ui-exchange-\n // plugin`, and sets the path-scoped `vt_ui_ungit` HttpOnly\n // cookie. Fragments never reach the server, never land in\n // proxy access logs, and aren't sent on outbound sub-request\n // Referer headers — matches the F5 `/ui/<plugin>` flow.\n const initial: IframeTokenResponse = await mintIframeToken(\n agentId,\n profile,\n '/api/plugins/ungit/ui'\n );\n if (cancelled) return;\n const baseUrl = `${tunnelUrl.replace(/\\/$/, '')}/api/plugins/ungit/ui`;\n setUngitIframeUrl(appendIframeTokenFragment(baseUrl, initial.token));\n handle = scheduleIframeTokenRefresh(\n agentId,\n profile,\n '/api/plugins/ungit/ui',\n initial,\n (next: IframeTokenResponse) => {\n setUngitIframeUrl(appendIframeTokenFragment(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 ) : !tunnelUrl || !apiKey ? (\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 ) : !tunnelUrl || !apiKey ? (\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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"}
@@ -43,10 +43,7 @@ function c(e, t) {
43
43
  let n = e.indexOf("#");
44
44
  return `${n === -1 ? e : e.slice(0, n)}#vt=${encodeURIComponent(t)}`;
45
45
  }
46
- function l(e, t) {
47
- return `${e}${e.includes("?") ? "&" : "?"}vt=${encodeURIComponent(t)}`;
48
- }
49
46
  //#endregion
50
- export { c as appendIframeTokenFragment, l as appendIframeTokenParam, i as mintIframeToken, a as mintScopedIframeToken, o as scheduleIframeTokenRefresh, s as scheduleScopedIframeTokenRefresh };
47
+ export { c as appendIframeTokenFragment, i as mintIframeToken, a as mintScopedIframeToken, o as scheduleIframeTokenRefresh, s as scheduleScopedIframeTokenRefresh };
51
48
 
52
49
  //# 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\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 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 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. Legacy\n * `appendIframeTokenParam(url, token)` is preserved only for the Ungit\n * surface at `/api/plugins/ungit/ui`, which does not run behind the\n * /ui/<plugin> cookie shim and is governed by the ungit plugin's own\n * apiKey/publicPaths contract.\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 * @deprecated Use {@link appendIframeTokenFragment} for any new iframe\n * embed. This helper places the token in the URL query string and is\n * retained ONLY for the Ungit surface (`/api/plugins/ungit/ui`) whose\n * iframe loads via the ungit plugin's own proxy and does not benefit\n * from the F5 /ui/<plugin> cookie shim. Audit F5 mandated tokens be\n * removed from URLs for plugin iframes; the Ungit case is tracked as a\n * separate cleanup once that surface has its own equivalent shim.\n */\nexport function appendIframeTokenParam(url: string, token: string): string {\n const sep = url.includes('?') ? '&' : '?';\n return `${url}${sep}vt=${encodeURIComponent(token)}`;\n}\n"],"mappings":";;AAkBA,IAAM,IAAsB,KACtB,IAAoB;AAoB1B,eAAsB,EACpB,GACA,GACA,GACA,IAAqB,GACS;CAC9B,IAAM,IAAM,KAAK,IAAI,GAAG,KAAK,IAAA,KAAkC,KAAK,MAAM,EAAW,CAAC,CAAC;AACvF,QAAO,EACL,GACA,EAAa,GAAS,sBAAsB,EAC5C,QACA;EAAE;EAAY,YAAY;EAAK,CAChC;;AAaH,eAAsB,EACpB,GACA,GACA,GACA,IAAqB,GACS;CAC9B,IAAM,IAAM,KAAK,IAAI,GAAG,KAAK,IAAA,KAAkC,KAAK,MAAM,EAAW,CAAC,CAAC;AACvF,QAAO,EACL,GACA,EAAa,GAAS,sBAAsB,EAC5C,QACA;EAAE;EAAc,YAAY;EAAK,CAClC;;AAUH,SAAgB,EACd,GACA,GACA,GACA,GACA,GACA,GACA,IAAqB,GACS;CAC9B,IAAI,IAAY,IACZ,IAA8C,MAE5C,IAAc,KAAK,MAAM,EAAQ,UAAU,EAC3C,IAAQ,KAAK,IAAI,KAAO,IAAc,KAAK,KAAK,GAAG,EAAkB;AAe3E,QAbA,IAAQ,iBAAiB;AACnB,OACJ,EAAgB,GAAS,GAAS,GAAY,EAAW,CACtD,MAAM,MAAS;AACV,QACJ,EAAU,EAAK;IACf,CACD,OAAO,MAAiB;AACnB,QACJ,IAAU,aAAe,QAAQ,IAAU,MAAM,OAAO,EAAI,CAAC,CAAC;IAC9D;IACH,EAAM,EAEF,EACL,cAAc;AAEZ,EADA,IAAY,IACR,MAAU,SACZ,aAAa,EAAM,EACnB,IAAQ;IAGb;;AAUH,SAAgB,EACd,GACA,GACA,GACA,GACA,GACA,GACA,IAAqB,GACS;CAC9B,IAAI,IAAY,IACZ,IAA8C,MAE5C,IAAc,KAAK,MAAM,EAAQ,UAAU,EAC3C,IAAQ,KAAK,IAAI,KAAO,IAAc,KAAK,KAAK,GAAG,EAAkB;AAe3E,QAbA,IAAQ,iBAAiB;AACnB,OACJ,EAAsB,GAAS,GAAS,GAAc,EAAW,CAC9D,MAAM,MAAS;AACV,QACJ,EAAU,EAAK;IACf,CACD,OAAO,MAAiB;AACnB,QACJ,IAAU,aAAe,QAAQ,IAAU,MAAM,OAAO,EAAI,CAAC,CAAC;IAC9D;IACH,EAAM,EAEF,EACL,cAAc;AAEZ,EADA,IAAY,IACR,MAAU,SACZ,aAAa,EAAM,EACnB,IAAQ;IAGb;;AAqBH,SAAgB,EAA0B,GAAa,GAAuB;CAE5E,IAAM,IAAU,EAAI,QAAQ,IAAI;AAEhC,QAAO,GADM,MAAY,KAAK,IAAM,EAAI,MAAM,GAAG,EAAQ,CAC1C,MAAM,mBAAmB,EAAM;;AAYhD,SAAgB,EAAuB,GAAa,GAAuB;AAEzE,QAAO,GAAG,IADE,EAAI,SAAS,IAAI,GAAG,MAAM,IAClB,KAAK,mBAAmB,EAAM"}
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\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 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 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"],"mappings":";;AAkBA,IAAM,IAAsB,KACtB,IAAoB;AAoB1B,eAAsB,EACpB,GACA,GACA,GACA,IAAqB,GACS;CAC9B,IAAM,IAAM,KAAK,IAAI,GAAG,KAAK,IAAA,KAAkC,KAAK,MAAM,EAAW,CAAC,CAAC;AACvF,QAAO,EACL,GACA,EAAa,GAAS,sBAAsB,EAC5C,QACA;EAAE;EAAY,YAAY;EAAK,CAChC;;AAaH,eAAsB,EACpB,GACA,GACA,GACA,IAAqB,GACS;CAC9B,IAAM,IAAM,KAAK,IAAI,GAAG,KAAK,IAAA,KAAkC,KAAK,MAAM,EAAW,CAAC,CAAC;AACvF,QAAO,EACL,GACA,EAAa,GAAS,sBAAsB,EAC5C,QACA;EAAE;EAAc,YAAY;EAAK,CAClC;;AAUH,SAAgB,EACd,GACA,GACA,GACA,GACA,GACA,GACA,IAAqB,GACS;CAC9B,IAAI,IAAY,IACZ,IAA8C,MAE5C,IAAc,KAAK,MAAM,EAAQ,UAAU,EAC3C,IAAQ,KAAK,IAAI,KAAO,IAAc,KAAK,KAAK,GAAG,EAAkB;AAe3E,QAbA,IAAQ,iBAAiB;AACnB,OACJ,EAAgB,GAAS,GAAS,GAAY,EAAW,CACtD,MAAM,MAAS;AACV,QACJ,EAAU,EAAK;IACf,CACD,OAAO,MAAiB;AACnB,QACJ,IAAU,aAAe,QAAQ,IAAU,MAAM,OAAO,EAAI,CAAC,CAAC;IAC9D;IACH,EAAM,EAEF,EACL,cAAc;AAEZ,EADA,IAAY,IACR,MAAU,SACZ,aAAa,EAAM,EACnB,IAAQ;IAGb;;AAUH,SAAgB,EACd,GACA,GACA,GACA,GACA,GACA,GACA,IAAqB,GACS;CAC9B,IAAI,IAAY,IACZ,IAA8C,MAE5C,IAAc,KAAK,MAAM,EAAQ,UAAU,EAC3C,IAAQ,KAAK,IAAI,KAAO,IAAc,KAAK,KAAK,GAAG,EAAkB;AAe3E,QAbA,IAAQ,iBAAiB;AACnB,OACJ,EAAsB,GAAS,GAAS,GAAc,EAAW,CAC9D,MAAM,MAAS;AACV,QACJ,EAAU,EAAK;IACf,CACD,OAAO,MAAiB;AACnB,QACJ,IAAU,aAAe,QAAQ,IAAU,MAAM,OAAO,EAAI,CAAC,CAAC;IAC9D;IACH,EAAM,EAEF,EACL,cAAc;AAEZ,EADA,IAAY,IACR,MAAU,SACZ,aAAa,EAAM,EACnB,IAAQ;IAGb;;AAuBH,SAAgB,EAA0B,GAAa,GAAuB;CAE5E,IAAM,IAAU,EAAI,QAAQ,IAAI;AAEhC,QAAO,GADM,MAAY,KAAK,IAAM,EAAI,MAAM,GAAG,EAAQ,CAC1C,MAAM,mBAAmB,EAAM"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-vibecontrols",
3
- "version": "2026.528.11",
3
+ "version": "2026.529.1",
4
4
  "description": "VibeControls microfrontend for Burdenoff products",
5
5
  "type": "module",
6
6
  "files": [