@replicated/portal-components 0.0.12 → 0.0.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.
Files changed (53) hide show
  1. package/components/metadata/registry.json +2 -2
  2. package/components/metadata/registry.md +2 -2
  3. package/dist/actions/index.d.mts +26 -4
  4. package/dist/actions/index.d.ts +26 -4
  5. package/dist/actions/index.js +171 -124
  6. package/dist/actions/index.js.map +1 -1
  7. package/dist/airgap-instances.js.map +1 -1
  8. package/dist/esm/actions/index.js +170 -124
  9. package/dist/esm/actions/index.js.map +1 -1
  10. package/dist/esm/airgap-instances.js.map +1 -1
  11. package/dist/esm/helm-install-wizard.js +15 -9
  12. package/dist/esm/helm-install-wizard.js.map +1 -1
  13. package/dist/esm/index.js +127 -108
  14. package/dist/esm/index.js.map +1 -1
  15. package/dist/esm/install-actions.js +42 -47
  16. package/dist/esm/install-actions.js.map +1 -1
  17. package/dist/esm/instance-card.js.map +1 -1
  18. package/dist/esm/license-details.js +20 -10
  19. package/dist/esm/license-details.js.map +1 -1
  20. package/dist/esm/linux-install-wizard.js +26 -47
  21. package/dist/esm/linux-install-wizard.js.map +1 -1
  22. package/dist/esm/online-instance-list.js.map +1 -1
  23. package/dist/esm/support-card.js +18 -49
  24. package/dist/esm/support-card.js.map +1 -1
  25. package/dist/esm/top-nav.js +13 -31
  26. package/dist/esm/top-nav.js.map +1 -1
  27. package/dist/esm/update-layout.js +13 -31
  28. package/dist/esm/update-layout.js.map +1 -1
  29. package/dist/esm/utils/index.js +14 -10
  30. package/dist/esm/utils/index.js.map +1 -1
  31. package/dist/helm-install-wizard.js +15 -9
  32. package/dist/helm-install-wizard.js.map +1 -1
  33. package/dist/index.d.mts +1 -1
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +126 -106
  36. package/dist/index.js.map +1 -1
  37. package/dist/install-actions.js +43 -48
  38. package/dist/install-actions.js.map +1 -1
  39. package/dist/instance-card.js.map +1 -1
  40. package/dist/license-details.js +20 -10
  41. package/dist/license-details.js.map +1 -1
  42. package/dist/linux-install-wizard.js +26 -47
  43. package/dist/linux-install-wizard.js.map +1 -1
  44. package/dist/online-instance-list.js.map +1 -1
  45. package/dist/support-card.js +18 -49
  46. package/dist/support-card.js.map +1 -1
  47. package/dist/top-nav.js +13 -31
  48. package/dist/top-nav.js.map +1 -1
  49. package/dist/update-layout.js +13 -31
  50. package/dist/update-layout.js.map +1 -1
  51. package/dist/utils/index.js +14 -10
  52. package/dist/utils/index.js.map +1 -1
  53. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/linux-install-wizard.tsx","../../src/utils/api-client.ts","../../src/actions/index.ts","../../src/actions/install.ts"],"names":[],"mappings":"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","file":"linux-install-wizard.js","sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { CreateServiceAccountResult } from \"../actions\";\nimport type {\n ChannelRelease,\n InstallInstructions,\n InstallStep,\n NetworkAvailability,\n CreateInstallOptionsResult,\n GetInstallOptionsResult,\n UpdateInstallOptionsResult,\n FetchChannelReleasesResult\n} from \"../actions/install\";\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nexport const LINUX_INSTALL_SERVICE_ACCOUNT_KEY = \"linux_install_service_account\";\nexport const LINUX_INSTALL_OPTIONS_KEY = \"linux_install_options\";\n\nexport interface LinuxInstallWizardProps {\n /** JWT token for authentication */\n token: string;\n /** Server action to create service account - REQUIRED */\n createServiceAccountAction: (name: string, token: string) => Promise<CreateServiceAccountResult>;\n /** Server action to fetch channel releases - REQUIRED for version dropdown */\n fetchChannelReleasesAction?: (token: string) => Promise<FetchChannelReleasesResult>;\n /** Server action to create install options - REQUIRED for tracking */\n createInstallOptionsAction?: (input: {\n token: string;\n installType: \"linux\";\n instanceName: string;\n serviceAccountId: string;\n networkAvailability: NetworkAvailability;\n isMultiNode?: boolean;\n channelId?: string;\n channelReleaseSequence?: number;\n }) => Promise<CreateInstallOptionsResult>;\n /** Server action to get install options - for resuming */\n getInstallOptionsAction?: (input: {\n token: string;\n installOptionsId: string;\n includeInstructions?: boolean;\n proxyUrl?: string;\n }) => Promise<GetInstallOptionsResult>;\n /** Server action to update install options - for version selection */\n updateInstallOptionsAction?: (input: {\n token: string;\n installOptionsId: string;\n channelId?: string;\n channelReleaseSequence?: number;\n adminConsoleUrl?: string | null;\n includeInstructions?: boolean;\n proxyUrl?: string;\n }) => Promise<UpdateInstallOptionsResult>;\n /** Callback when step changes */\n onStepChange?: (step: 1 | 2) => void;\n /** Callback when network availability changes */\n onNetworkChange?: (network: NetworkAvailability) => void;\n /** Callback when installOptionsId changes (for URL tracking) */\n onInstallOptionsIdChange?: (id: string | null) => void;\n /** Initial step to show (default: 1) */\n initialStep?: 1 | 2;\n /** Initial network availability (default: \"online\") */\n initialNetwork?: NetworkAvailability;\n /** Initial install options ID (for resuming) */\n initialInstallOptionsId?: string;\n /** Pre-fetched install options data (for SSR - skips client fetch) */\n initialInstallOptionsData?: GetInstallOptionsResult;\n /** Pre-fetched channel releases (for SSR - skips client fetch) */\n initialChannelReleases?: ChannelRelease[];\n}\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\nconst navigateTo = (href: string) => {\n try {\n if (typeof window === \"undefined\") {\n return;\n }\n window.location.assign(href);\n } catch (error) {\n console.error(\"[linux-install-wizard] navigation failed\", error);\n }\n};\n\n\nconst copyToClipboard = async (text: string): Promise<boolean> => {\n try {\n await navigator.clipboard.writeText(text);\n return true;\n } catch {\n return false;\n }\n};\n\n// =============================================================================\n// Sub-Components\n// =============================================================================\n\nconst StepIndicator = ({ step }: { step: 1 | 2 }) => (\n <div className=\"flex items-center justify-center gap-3\">\n <div\n className={`flex h-10 w-10 items-center justify-center rounded-full border-2 ${\n step > 1 ? \"border-gray-900 bg-gray-900 text-white\" : \"border-indigo-500\"\n }`}\n >\n {step > 1 ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 16 16\"\n className=\"h-3.5 w-3.5\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n >\n <path d=\"m3.5 8 3 3 6-6\" />\n </svg>\n ) : (\n <span className=\"h-2.5 w-2.5 rounded-full bg-indigo-500\" />\n )}\n </div>\n <div className={`h-0.5 w-12 ${step > 1 ? \"bg-gray-900\" : \"bg-gray-200\"}`} />\n <div\n className={`flex h-10 w-10 items-center justify-center rounded-full border-2 ${\n step === 2 ? \"border-gray-900\" : \"border-gray-200\"\n }`}\n >\n {step === 2 ? <span className=\"h-2.5 w-2.5 rounded-full bg-gray-900\" /> : null}\n </div>\n </div>\n);\n\nconst CodeBlock = ({ \n command, \n onCopy \n}: { \n command: string;\n onCopy?: () => void;\n}) => {\n const [copied, setCopied] = useState(false);\n\n const handleCopy = async () => {\n const success = await copyToClipboard(command);\n if (success) {\n setCopied(true);\n onCopy?.();\n setTimeout(() => setCopied(false), 2000);\n }\n };\n\n return (\n <div className=\"group relative ml-8 mt-2 min-w-0\">\n <pre className=\"overflow-x-auto whitespace-pre rounded-lg bg-gray-900 p-4 text-sm text-gray-100\">\n <code className=\"block\">{command}</code>\n </pre>\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"absolute right-2 top-2 rounded bg-gray-700 p-1.5 text-gray-300 opacity-0 transition hover:bg-gray-600 hover:text-white group-hover:opacity-100\"\n aria-label=\"Copy to clipboard\"\n >\n {copied ? (\n <svg className=\"h-4 w-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ) : (\n <svg className=\"h-4 w-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\" />\n </svg>\n )}\n </button>\n </div>\n );\n};\n\nconst VersionDropdown = ({\n releases,\n selectedRelease,\n onSelect,\n isLoading,\n error\n}: {\n releases: ChannelRelease[];\n selectedRelease: ChannelRelease | null;\n onSelect: (release: ChannelRelease) => void;\n isLoading: boolean;\n error: string | null;\n}) => {\n if (isLoading) {\n return (\n <div className=\"ml-8 flex items-center gap-2 text-sm text-gray-500\">\n <div className=\"h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-indigo-500\" />\n Loading versions...\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"ml-8 text-sm text-rose-600\">\n Failed to load versions: {error}\n </div>\n );\n }\n\n if (releases.length === 0) {\n return (\n <p className=\"ml-8 text-sm text-gray-500\">\n There is no Embedded Cluster installer in this release.\n </p>\n );\n }\n\n return (\n <div className=\"ml-8 space-y-2\">\n <label className=\"block text-sm text-gray-600\">App Version</label>\n <select\n value={selectedRelease?.channelSequence ?? \"\"}\n onChange={(e) => {\n const sequence = parseInt(e.target.value, 10);\n const release = releases.find(r => r.channelSequence === sequence);\n if (release) {\n onSelect(release);\n }\n }}\n className=\"portal-select w-full\"\n >\n {releases.map((release) => (\n <option key={release.channelSequence} value={release.channelSequence}>\n {release.versionLabel || `Sequence ${release.channelSequence}`}\n {release.channelName ? ` (${release.channelName})` : \"\"}\n </option>\n ))}\n </select>\n </div>\n );\n};\n\nconst CheckIcon = () => (\n <svg className=\"h-4 w-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2.5}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n);\n\nconst InstallationInstructions = ({\n instructions,\n isLoading,\n completedSteps = {}\n}: {\n instructions: InstallInstructions | null;\n isLoading: boolean;\n completedSteps?: Record<string, boolean>;\n}) => {\n // Only show loading spinner if we don't have any instructions yet\n if (isLoading && !instructions?.steps?.length) {\n return (\n <div className=\"space-y-4\">\n <h3 className=\"text-lg font-semibold text-gray-900\">Installation Instructions</h3>\n <div className=\"flex items-center gap-2 text-sm text-gray-500\">\n <div className=\"h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-indigo-500\" />\n Loading instructions...\n </div>\n </div>\n );\n }\n\n if (!instructions?.steps?.length) {\n return null;\n }\n\n return (\n <div className={`min-w-0 space-y-4 transition-opacity duration-150 ${isLoading ? \"opacity-60\" : \"\"}`}>\n <h3 className=\"text-lg font-semibold text-gray-900\">\n Installation Instructions\n {isLoading && (\n <span className=\"ml-2 inline-block h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-indigo-500\" />\n )}\n </h3>\n <ol className=\"min-w-0 space-y-6 text-sm text-gray-700\">\n {instructions.steps.map((step: InstallStep, index: number) => {\n // Steps that can show completion checkmarks (based on step_name from backend)\n // These are the steps that track progress: download_assets and install\n const completableStepNames = [\"download_assets\", \"install\"];\n const canComplete = completableStepNames.includes(step.step_name);\n const isCompleted = canComplete && completedSteps[step.step_name];\n return (\n <li key={step.step_number} className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n {/* Always show step number */}\n <span className=\"flex h-6 w-6 items-center justify-center rounded-full bg-indigo-100 font-semibold text-indigo-500\">\n {index + 2}\n </span>\n <span className=\"font-medium text-gray-900\">{step.title}</span>\n {/* Show checkmark to the right of title for completable steps */}\n {canComplete && (\n <span \n className={`flex h-5 w-5 items-center justify-center rounded-full ${\n isCompleted \n ? \"bg-green-500 text-white\" \n : \"bg-gray-200 text-gray-400\"\n }`}\n >\n <CheckIcon />\n </span>\n )}\n {isCompleted && (\n <span className=\"text-xs text-green-600\">Completed</span>\n )}\n </div>\n {step.description && (\n <p className=\"ml-8 text-gray-500\">{step.description}</p>\n )}\n {step.commands.map((command, cmdIndex) => (\n <CodeBlock key={cmdIndex} command={command} />\n ))}\n </li>\n );\n })}\n </ol>\n </div>\n );\n};\n\n// =============================================================================\n// Main Component\n// =============================================================================\n\nexport const LinuxInstallWizard = ({\n token,\n createServiceAccountAction,\n fetchChannelReleasesAction,\n createInstallOptionsAction,\n getInstallOptionsAction,\n updateInstallOptionsAction,\n onStepChange,\n onNetworkChange,\n onInstallOptionsIdChange,\n initialStep,\n initialNetwork,\n initialInstallOptionsId,\n initialInstallOptionsData,\n initialChannelReleases\n}: LinuxInstallWizardProps) => {\n // Basic wizard state - initialize from pre-fetched data if available\n const [step, setStep] = useState<1 | 2>(initialStep ?? 1);\n const [instanceName, setInstanceName] = useState(initialInstallOptionsData?.instance_name ?? \"\");\n const [networkAvailability, setNetworkAvailability] = useState<NetworkAvailability>(initialNetwork ?? \"online\");\n const [adminConsoleUrl, setAdminConsoleUrl] = useState(initialInstallOptionsData?.admin_console_url ?? \"https://localhost:30000\");\n const [proxyUrl, setProxyUrl] = useState(\"\");\n const [showErrors, setShowErrors] = useState(false);\n const [isCreatingServiceAccount, setIsCreatingServiceAccount] = useState(false);\n const [apiError, setApiError] = useState<string | null>(null);\n\n // Install options state - initialize from pre-fetched data if available\n const [installOptionsId, setInstallOptionsId] = useState<string | null>(initialInstallOptionsId ?? null);\n const [serviceAccountId, setServiceAccountId] = useState<string | null>(initialInstallOptionsData?.service_account_id ?? null);\n // Track the original instance name used when service account was created (for back button handling)\n const [originalInstanceName, setOriginalInstanceName] = useState<string | null>(initialInstallOptionsData?.instance_name ?? null);\n\n // Channel releases state - initialize from pre-fetched data if available\n const [releases, setReleases] = useState<ChannelRelease[]>(initialChannelReleases ?? []);\n const [selectedRelease, setSelectedRelease] = useState<ChannelRelease | null>(() => {\n // If we have pre-fetched data, find the matching release\n if (initialInstallOptionsData?.channel_id && initialInstallOptionsData?.channel_release_sequence && initialChannelReleases) {\n return initialChannelReleases.find(\n r => r.channelId === initialInstallOptionsData.channel_id && \n r.channelSequence === initialInstallOptionsData.channel_release_sequence\n ) ?? null;\n }\n return null;\n });\n const [isLoadingReleases, setIsLoadingReleases] = useState(false);\n const [releasesError, setReleasesError] = useState<string | null>(null);\n\n // Instructions state - initialize from pre-fetched data if available\n const [instructions, setInstructions] = useState<InstallInstructions | null>(initialInstallOptionsData?.instructions ?? null);\n const [isLoadingInstructions, setIsLoadingInstructions] = useState(false);\n const [completedSteps, setCompletedSteps] = useState<Record<string, boolean>>({});\n\n // Refs for tracking state - mark as loaded if we have pre-fetched data\n const hasLoadedReleases = useRef(!!initialChannelReleases?.length);\n const hasResumedInstallation = useRef(!!initialInstallOptionsData);\n const lastUpdateRef = useRef<{ channelId?: string; sequence?: number }>(\n initialInstallOptionsData?.channel_id && initialInstallOptionsData?.channel_release_sequence\n ? { channelId: initialInstallOptionsData.channel_id, sequence: initialInstallOptionsData.channel_release_sequence }\n : {}\n );\n\n // Filter releases to only those with Embedded Cluster support\n const embeddedClusterReleases = useMemo(() => {\n return releases.filter(r => r.installationTypes?.embeddedCluster?.version);\n }, [releases]);\n\n // ==========================================================================\n // Effects - Sync with props\n // ==========================================================================\n\n // Only sync step from external prop when it changes (for URL-based navigation)\n // Using a ref to track the previous initialStep to avoid resetting on internal step changes\n const prevInitialStepRef = useRef(initialStep);\n useEffect(() => {\n if (initialStep !== undefined && initialStep !== prevInitialStepRef.current) {\n setStep(initialStep);\n }\n prevInitialStepRef.current = initialStep;\n }, [initialStep]);\n\n useEffect(() => {\n if (initialNetwork !== undefined && initialNetwork !== networkAvailability) {\n setNetworkAvailability(initialNetwork);\n }\n }, [initialNetwork, networkAvailability]);\n\n useEffect(() => {\n onStepChange?.(step);\n }, [step, onStepChange]);\n\n useEffect(() => {\n onNetworkChange?.(networkAvailability);\n }, [networkAvailability, onNetworkChange]);\n\n useEffect(() => {\n onInstallOptionsIdChange?.(installOptionsId);\n }, [installOptionsId, onInstallOptionsIdChange]);\n\n // ==========================================================================\n // Effects - Load releases on step 2\n // ==========================================================================\n\n useEffect(() => {\n if (step !== 2 || !fetchChannelReleasesAction || hasLoadedReleases.current) {\n return;\n }\n\n const loadReleases = async () => {\n setIsLoadingReleases(true);\n setReleasesError(null);\n\n try {\n const result = await fetchChannelReleasesAction(token);\n setReleases(result.channelReleases || []);\n hasLoadedReleases.current = true;\n\n // Auto-select first release with EC support\n const ecReleases = (result.channelReleases || []).filter(\n r => r.installationTypes?.embeddedCluster?.version\n );\n const firstRelease = ecReleases[0];\n if (firstRelease && !selectedRelease) {\n setSelectedRelease(firstRelease);\n }\n } catch (error) {\n console.error(\"[linux-install-wizard] Failed to load releases\", error);\n setReleasesError(error instanceof Error ? error.message : \"Failed to load releases\");\n } finally {\n setIsLoadingReleases(false);\n }\n };\n\n loadReleases();\n }, [step, token, fetchChannelReleasesAction, selectedRelease]);\n\n // Auto-select first release when pre-fetched releases are available (for new installs)\n const hasAutoSelectedRelease = useRef(false);\n useEffect(() => {\n if (hasAutoSelectedRelease.current || selectedRelease) {\n return;\n }\n const firstRelease = embeddedClusterReleases[0];\n if (step === 2 && firstRelease) {\n setSelectedRelease(firstRelease);\n hasAutoSelectedRelease.current = true;\n }\n }, [step, embeddedClusterReleases, selectedRelease]);\n\n // ==========================================================================\n // Effects - Update install options when release changes\n // ==========================================================================\n\n useEffect(() => {\n if (!selectedRelease || !installOptionsId || !updateInstallOptionsAction) {\n return;\n }\n\n const channelId = selectedRelease.channelId;\n const sequence = selectedRelease.channelSequence;\n\n // Skip if we already updated for this release\n if (\n lastUpdateRef.current.channelId === channelId &&\n lastUpdateRef.current.sequence === sequence\n ) {\n return;\n }\n\n const updateOptions = async () => {\n setIsLoadingInstructions(true);\n\n try {\n const result = await updateInstallOptionsAction({\n token,\n installOptionsId,\n channelId,\n channelReleaseSequence: sequence,\n includeInstructions: true,\n proxyUrl: networkAvailability === \"proxy\" ? proxyUrl : undefined\n });\n\n lastUpdateRef.current = { channelId, sequence };\n\n if (result.instructions) {\n setInstructions(result.instructions);\n }\n } catch (error) {\n console.error(\"[linux-install-wizard] Failed to update install options\", error);\n setApiError(error instanceof Error ? error.message : \"Failed to update install options\");\n } finally {\n setIsLoadingInstructions(false);\n }\n };\n\n updateOptions();\n }, [selectedRelease, installOptionsId, token, updateInstallOptionsAction, networkAvailability, proxyUrl]);\n\n // ==========================================================================\n // Effects - Resume from install options ID\n // ==========================================================================\n\n useEffect(() => {\n // Skip if we already have pre-fetched data or already resumed\n if (hasResumedInstallation.current) {\n return;\n }\n if (!initialInstallOptionsId || !getInstallOptionsAction || step !== 2) {\n return;\n }\n\n hasResumedInstallation.current = true;\n\n const resumeInstallation = async () => {\n setIsLoadingInstructions(true);\n\n try {\n const result = await getInstallOptionsAction({\n token,\n installOptionsId: initialInstallOptionsId,\n includeInstructions: true,\n proxyUrl: networkAvailability === \"proxy\" ? proxyUrl : undefined\n });\n\n // Restore state from install options\n if (result.instance_name) {\n setInstanceName(result.instance_name);\n }\n if (result.service_account_id) {\n setServiceAccountId(result.service_account_id);\n }\n if (result.instructions) {\n setInstructions(result.instructions);\n }\n if (result.admin_console_url) {\n setAdminConsoleUrl(result.admin_console_url);\n }\n\n // Find and set the selected release\n if (result.channel_id && result.channel_release_sequence) {\n const matchingRelease = releases.find(\n r => r.channelId === result.channel_id && \n r.channelSequence === result.channel_release_sequence\n );\n if (matchingRelease) {\n setSelectedRelease(matchingRelease);\n lastUpdateRef.current = {\n channelId: result.channel_id,\n sequence: result.channel_release_sequence\n };\n }\n }\n } catch (error) {\n console.error(\"[linux-install-wizard] Failed to resume installation\", error);\n setApiError(error instanceof Error ? error.message : \"Failed to resume installation\");\n } finally {\n setIsLoadingInstructions(false);\n }\n };\n\n resumeInstallation();\n }, [initialInstallOptionsId, getInstallOptionsAction, token, step, releases, networkAvailability, proxyUrl]);\n\n // ==========================================================================\n // Effects - Poll for step completion status\n // ==========================================================================\n\n useEffect(() => {\n // Only poll when on Step 2 with an installOptionsId and getInstallOptionsAction available\n if (step !== 2 || !installOptionsId || !getInstallOptionsAction) {\n return;\n }\n\n // Check if both steps are already completed - no need to poll\n if (completedSteps[\"download_assets\"] && completedSteps[\"install\"]) {\n return;\n }\n\n const pollInterval = setInterval(async () => {\n try {\n const result = await getInstallOptionsAction({\n token,\n installOptionsId,\n includeInstructions: false, // We don't need instructions for status polling\n });\n\n // Update completed steps based on timestamps\n const newCompletedSteps: Record<string, boolean> = {};\n \n if (result.assets_downloaded_at) {\n newCompletedSteps[\"download_assets\"] = true;\n }\n if (result.installation_completed_at) {\n newCompletedSteps[\"install\"] = true;\n }\n\n // Only update state if something changed\n if (\n newCompletedSteps[\"download_assets\"] !== completedSteps[\"download_assets\"] ||\n newCompletedSteps[\"install\"] !== completedSteps[\"install\"]\n ) {\n setCompletedSteps(prev => ({ ...prev, ...newCompletedSteps }));\n }\n\n // Stop polling if both are complete\n if (newCompletedSteps[\"download_assets\"] && newCompletedSteps[\"install\"]) {\n clearInterval(pollInterval);\n }\n } catch {\n // Silently ignore polling errors\n }\n }, 2000);\n\n return () => clearInterval(pollInterval);\n }, [step, installOptionsId, getInstallOptionsAction, token, completedSteps]);\n\n // ==========================================================================\n // Handlers\n // ==========================================================================\n\n const handleContinue = async () => {\n console.debug(\"[linux-install-wizard] handleContinue called, instanceName:\", JSON.stringify(instanceName));\n if (!instanceName.trim()) {\n console.debug(\"[linux-install-wizard] Validation failed: instanceName is empty\");\n setShowErrors(true);\n return;\n }\n\n if (!token) {\n setApiError(\"Configuration error: authentication token is required\");\n console.error(\"[linux-install-wizard] token prop is required but was not provided\");\n return;\n }\n\n setShowErrors(false);\n setApiError(null);\n setIsCreatingServiceAccount(true);\n\n try {\n const trimmedInstanceName = instanceName.trim();\n \n // Branch 3: Service account exists AND instance name is the same (user went back but kept same name)\n // Just update install options, don't create new service account\n if (serviceAccountId && originalInstanceName === trimmedInstanceName && installOptionsId && updateInstallOptionsAction) {\n console.debug(\"[linux-install-wizard] Reusing existing service account, updating install options...\");\n \n const firstRelease = embeddedClusterReleases[0];\n \n const result = await updateInstallOptionsAction({\n token,\n installOptionsId,\n channelId: firstRelease?.channelId,\n channelReleaseSequence: firstRelease?.channelSequence,\n includeInstructions: true\n });\n \n if (firstRelease) {\n setSelectedRelease(firstRelease);\n lastUpdateRef.current = {\n channelId: firstRelease.channelId,\n sequence: firstRelease.channelSequence\n };\n }\n \n if (result.instructions) {\n setInstructions(result.instructions);\n }\n \n console.debug(\"[linux-install-wizard] Transitioning to step 2 (reused service account)\");\n setStep(2);\n return;\n }\n \n // Branch 1 & 2: Need to create new service account\n // (Either no service account exists, or instance name changed)\n \n // Delete existing data from session storage\n if (typeof window !== \"undefined\" && window.sessionStorage) {\n window.sessionStorage.removeItem(LINUX_INSTALL_SERVICE_ACCOUNT_KEY);\n window.sessionStorage.removeItem(LINUX_INSTALL_OPTIONS_KEY);\n }\n\n // Create service account\n const saData = await createServiceAccountAction(trimmedInstanceName, token);\n\n // Store in session storage\n if (typeof window !== \"undefined\" && window.sessionStorage) {\n window.sessionStorage.setItem(LINUX_INSTALL_SERVICE_ACCOUNT_KEY, JSON.stringify(saData));\n }\n\n setServiceAccountId(saData.service_account.id);\n setOriginalInstanceName(trimmedInstanceName);\n\n // Create install options (if action provided)\n if (createInstallOptionsAction) {\n console.debug(\"[linux-install-wizard] Creating install options...\");\n \n // Get the first available release to include with install options\n // This allows instructions to be generated immediately\n const firstRelease = embeddedClusterReleases[0];\n \n const installOptionsResult = await createInstallOptionsAction({\n token,\n installType: \"linux\",\n instanceName: trimmedInstanceName,\n serviceAccountId: saData.service_account.id,\n networkAvailability: networkAvailability,\n isMultiNode: false,\n channelId: firstRelease?.channelId,\n channelReleaseSequence: firstRelease?.channelSequence\n });\n console.debug(\"[linux-install-wizard] Install options result:\", installOptionsResult);\n \n // Set the selected release to match what we sent\n if (firstRelease) {\n setSelectedRelease(firstRelease);\n lastUpdateRef.current = {\n channelId: firstRelease.channelId,\n sequence: firstRelease.channelSequence\n };\n }\n\n // Handle both response formats: { install_options: { id: ... } } and { id: ... }\n const optionsId = installOptionsResult.install_options?.id ?? (installOptionsResult as any).id;\n if (!optionsId) {\n throw new Error(\"Install options response did not contain an ID\");\n }\n setInstallOptionsId(optionsId);\n\n // Store in session storage\n if (typeof window !== \"undefined\" && window.sessionStorage) {\n window.sessionStorage.setItem(LINUX_INSTALL_OPTIONS_KEY, JSON.stringify(installOptionsResult));\n }\n\n // If we got instructions back, store them\n if (installOptionsResult.instructions) {\n setInstructions(installOptionsResult.instructions);\n }\n\n // Notify parent of new install options ID\n onInstallOptionsIdChange?.(optionsId);\n }\n\n console.debug(\"[linux-install-wizard] Transitioning to step 2\");\n setStep(2);\n } catch (error) {\n console.error(\"[linux-install-wizard] Failed to continue\", error);\n const errorMessage = error instanceof Error ? error.message : \"Failed to continue\";\n // Provide a friendlier error message for \"already exists\" errors\n if (errorMessage.toLowerCase().includes(\"already exists\")) {\n setApiError(\"Instance name must be unique. Please choose a different name.\");\n } else {\n setApiError(errorMessage);\n }\n } finally {\n setIsCreatingServiceAccount(false);\n }\n };\n\n const handleBack = () => {\n // Clear errors when going back\n setApiError(null);\n setShowErrors(false);\n setStep(1);\n };\n\n const handleFinish = useCallback(async () => {\n // Update admin console URL if changed\n if (installOptionsId && updateInstallOptionsAction && adminConsoleUrl) {\n try {\n await updateInstallOptionsAction({\n token,\n installOptionsId,\n adminConsoleUrl\n });\n } catch (error) {\n console.error(\"[linux-install-wizard] Failed to save admin console URL\", error);\n // Don't block navigation on this error\n }\n }\n\n navigateTo(\"/update\");\n }, [installOptionsId, updateInstallOptionsAction, adminConsoleUrl, token]);\n\n const handleReleaseSelect = useCallback((release: ChannelRelease) => {\n setSelectedRelease(release);\n }, []);\n\n const isProxy = networkAvailability === \"proxy\";\n\n // ==========================================================================\n // Render Step 2\n // ==========================================================================\n\n if (step === 2) {\n return (\n <div className=\"space-y-6\">\n <StepIndicator step={2} />\n <div className=\"overflow-hidden rounded-2xl border border-gray-100 bg-gray-50 p-6\">\n <div className=\"min-w-0 space-y-6\">\n <div className=\"min-w-0\">\n <h2 className=\"text-xl font-semibold text-gray-900\">\n {isProxy ? \"Linux Single Node Proxy Install\" : \"Linux Single Node Online Install\"}\n </h2>\n\n <div className=\"mt-6 space-y-6\">\n {/* Step 1: Version Selection */}\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-indigo-500\">\n <span className=\"flex h-6 w-6 items-center justify-center rounded-full bg-indigo-100 font-semibold\">1</span>\n <span className=\"font-medium text-gray-900\">Select a version</span>\n </div>\n <VersionDropdown\n releases={embeddedClusterReleases}\n selectedRelease={selectedRelease}\n onSelect={handleReleaseSelect}\n isLoading={isLoadingReleases}\n error={releasesError}\n />\n </div>\n\n {/* Step 2 (Proxy only): Configure proxy URL */}\n {isProxy && (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-indigo-500\">\n <span className=\"flex h-6 w-6 items-center justify-center rounded-full bg-indigo-100 font-semibold\">2</span>\n <span className=\"font-medium text-gray-900\">Configure proxy URL</span>\n </div>\n <input\n value={proxyUrl}\n onChange={(event) => setProxyUrl(event.target.value)}\n placeholder=\"Enter proxy URL\"\n className=\"portal-input ml-8 w-full\"\n />\n </div>\n )}\n\n {/* Installation Instructions */}\n {selectedRelease && (\n <InstallationInstructions\n instructions={instructions}\n isLoading={isLoadingInstructions}\n completedSteps={completedSteps}\n />\n )}\n\n {/* Admin Console URL (optional) */}\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-indigo-500\">\n <span className=\"flex h-6 w-6 items-center justify-center rounded-full bg-indigo-100 font-semibold\">\n {(instructions?.steps?.length ?? 0) + (isProxy ? 3 : 2)}\n </span>\n <span className=\"font-medium text-gray-900\">(Optional) Add the Admin Console URL</span>\n </div>\n <input\n value={adminConsoleUrl}\n onChange={(event) => setAdminConsoleUrl(event.target.value)}\n placeholder=\"https://admin-console.example.com\"\n className=\"portal-input ml-8 w-full\"\n />\n </div>\n </div>\n </div>\n\n {apiError && (\n <div className=\"rounded-xl border border-rose-200 bg-rose-50 p-3 text-sm text-rose-600\">\n {apiError}\n </div>\n )}\n\n <div className=\"flex items-center justify-between text-sm text-gray-500\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"rounded-xl px-4 py-2 font-medium text-gray-500 transition hover:bg-gray-100\"\n >\n Back\n </button>\n <span>Step 2 of 2</span>\n <button\n type=\"button\"\n onClick={handleFinish}\n className=\"rounded-xl bg-indigo-500 px-4 py-2 font-medium text-white transition hover:bg-indigo-600\"\n >\n Finish\n </button>\n </div>\n\n <p className=\"text-center text-xs text-gray-400\">\n Having trouble? <a className=\"text-indigo-500\" href=\"#\">Open an issue.</a>\n </p>\n </div>\n </div>\n </div>\n );\n }\n\n // ==========================================================================\n // Render Step 1\n // ==========================================================================\n\n return (\n <div className=\"space-y-6\">\n <StepIndicator step={1} />\n\n <div className=\"rounded-2xl border border-gray-100 bg-gray-50 p-6\">\n <div className=\"space-y-4\">\n <label className=\"block text-sm font-medium text-gray-700\">\n Instance Name\n <input\n value={instanceName}\n onChange={(event) => setInstanceName(event.target.value)}\n placeholder=\"Instance nickname\"\n aria-invalid={showErrors && !instanceName.trim()}\n className={`portal-input mt-1 w-full ${\n showErrors && !instanceName.trim()\n ? \"border-rose-400 focus:border-rose-400 focus:ring-rose-200\"\n : \"\"\n }`}\n />\n {showErrors && !instanceName.trim() ? (\n <span className=\"mt-1 block text-xs text-rose-500\">Instance name is required.</span>\n ) : null}\n </label>\n\n <fieldset className=\"space-y-2\">\n <legend className=\"text-sm font-medium text-gray-700\">Network Availability</legend>\n <label className=\"flex items-center gap-3 text-sm text-gray-600\">\n <input\n type=\"radio\"\n name=\"network-availability\"\n checked={networkAvailability === \"online\"}\n onChange={() => setNetworkAvailability(\"online\")}\n className=\"portal-radio\"\n />\n Outbound requests allowed\n </label>\n <label className=\"flex items-center gap-3 text-sm text-gray-600\">\n <input\n type=\"radio\"\n name=\"network-availability\"\n checked={networkAvailability === \"proxy\"}\n onChange={() => setNetworkAvailability(\"proxy\")}\n className=\"portal-radio\"\n />\n Outbound requests require HTTPS Proxy\n </label>\n <label className=\"flex items-center gap-3 text-sm text-gray-600\">\n <input\n type=\"radio\"\n name=\"network-availability\"\n checked={networkAvailability === \"airgap\"}\n onChange={() => setNetworkAvailability(\"airgap\")}\n className=\"portal-radio\"\n />\n No outbound requests allowed (air gap)\n </label>\n </fieldset>\n\n {apiError ? (\n <div className=\"rounded-xl border border-rose-200 bg-rose-50 p-3 text-sm text-rose-600\">\n {apiError}\n </div>\n ) : null}\n </div>\n\n <div className=\"mt-6 flex items-center justify-between text-sm text-gray-500\">\n <span>Step 1 of 2</span>\n <button\n type=\"button\"\n onClick={handleContinue}\n disabled={isCreatingServiceAccount}\n className=\"rounded-xl bg-indigo-500 px-4 py-2 font-medium text-white transition hover:bg-indigo-600 disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {isCreatingServiceAccount ? \"Creating...\" : \"Continue\"}\n </button>\n </div>\n </div>\n </div>\n );\n};\n\nLinuxInstallWizard.displayName = \"LinuxInstallWizard\";\n","/**\n * Centralized API client utility for handling authenticated requests\n * with automatic 401 detection, cookie deletion, and redirect.\n */\n\nexport interface ApiFetchOptions extends RequestInit {\n token?: string;\n}\n\nexport class UnauthorizedError extends Error {\n constructor(message = \"Unauthorized\") {\n super(message);\n this.name = \"UnauthorizedError\";\n }\n}\n\n/**\n * Helper to check if an error is a Next.js redirect error.\n * These errors should NOT be caught as they control navigation flow.\n */\nexport function isRedirectError(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"digest\" in error &&\n typeof (error as { digest?: unknown }).digest === \"string\" &&\n (error as { digest: string }).digest.startsWith(\"NEXT_REDIRECT\")\n );\n}\n\n/**\n * Fetch wrapper that automatically handles error responses by:\n * - 401: Redirecting to \"/\" with expired parameter\n * - 502, 503, 504: Redirecting to \"/error\" with status code\n * \n * This function should be used for all authenticated API calls in server actions.\n * \n * IMPORTANT: This must be called from a Server Action context, not directly from\n * a Server Component, because it modifies cookies.\n */\nexport async function authenticatedFetch(\n url: string,\n options: ApiFetchOptions = {}\n): Promise<Response> {\n const { token, ...fetchOptions } = options;\n\n // Add authorization header if token is provided\n const headers = new Headers(fetchOptions.headers);\n if (token) {\n headers.set(\"authorization\", `Bearer ${token}`);\n }\n\n const response = await fetch(url, {\n ...fetchOptions,\n headers\n });\n\n // Handle 401 Unauthorized\n if (response.status === 401) {\n await handle401();\n }\n\n // Handle server errors (502, 503, 504) by redirecting to error page\n if (response.status === 502 || response.status === 503 || response.status === 504) {\n await handleServerError(response.status);\n }\n\n return response;\n}\n\n/**\n * Handles 401 unauthorized responses by redirecting to the root path with a logout parameter.\n * \n * Note: We cannot delete cookies here because this is called during Server Component\n * render, not from a true Server Action invocation.\n * \n * The home page MUST check for the `?expired=1` parameter and delete the portal_session\n * cookie when present to avoid infinite loops. Example:\n * \n * ```typescript\n * // In your home page (app/page.tsx)\n * import { cookies } from \"next/headers\";\n * \n * export default async function Home({ searchParams }: { searchParams: { expired?: string } }) {\n * if (searchParams.expired === \"1\") {\n * const cookieStore = await cookies();\n * cookieStore.delete(\"portal_session\");\n * }\n * \n * const sessionStore = await cookies();\n * const session = sessionStore.get(\"portal_session\");\n * // ... rest of your logic\n * }\n * ```\n */\nasync function handle401(): Promise<never> {\n const { redirect } = await import(\"next/navigation\");\n \n // Redirect with expired parameter so the home page can delete the cookie\n return redirect(\"/?expired=1\");\n}\n\n/**\n * Handles server errors (502, 503, 504) by redirecting to the error page.\n * \n * The error page route should be created at `app/error/page.tsx` in the consuming\n * application and use the ErrorPage component. Example:\n * \n * ```typescript\n * // In your error page (app/error/page.tsx)\n * import { ErrorPage } from \"@replicated/portal-components/error-page\";\n * \n * export default function Error({ \n * searchParams \n * }: { \n * searchParams: { code?: string; source?: string } \n * }) {\n * const statusCode = searchParams.code ? parseInt(searchParams.code, 10) : undefined;\n * const sourceUrl = searchParams.source;\n * return <ErrorPage statusCode={statusCode} sourceUrl={sourceUrl} />;\n * }\n * ```\n */\nasync function handleServerError(statusCode: number): Promise<never> {\n const { redirect } = await import(\"next/navigation\");\n \n // Try to get the current URL from Next.js headers\n let sourceUrl: string | undefined;\n try {\n const { headers } = await import(\"next/headers\");\n const headersList = await headers();\n const referer = headersList.get(\"referer\");\n const host = headersList.get(\"host\");\n const pathname = headersList.get(\"x-invoke-path\") || headersList.get(\"x-forwarded-path\");\n \n if (referer) {\n sourceUrl = referer;\n } else if (host && pathname) {\n const protocol = headersList.get(\"x-forwarded-proto\") || \"https\";\n sourceUrl = `${protocol}://${host}${pathname}`;\n }\n } catch (error) {\n // If we can't get headers, continue without source URL\n console.debug(\"[portal-components] Could not determine source URL\", error);\n }\n \n // Redirect to error page with status code and source URL parameters\n const params = new URLSearchParams({ code: String(statusCode) });\n if (sourceUrl) {\n params.set(\"source\", sourceUrl);\n }\n \n return redirect(`/error?${params.toString()}`);\n}\n","/**\n * Light-weight type helpers for defining Server Actions that align with the\n * enterprise portal guardrails. The component library does not implement\n * specific actions, but it exports helpers so downstream portals can describe\n * their actions with consistent metadata.\n */\n\nimport { cache } from \"react\";\nimport { authenticatedFetch } from \"../utils/api-client\";\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Gets the base API origin from environment, with trailing slashes removed.\n */\nexport const getApiOrigin = (): string => {\n return (process.env.REPLICATED_APP_ORIGIN || \"https://replicated.app\").replace(/\\/+$/, \"\");\n};\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport type PortalActionVisibility = \"vendor\" | \"customer\";\n\nexport interface PortalActionContext {\n vendorId: string;\n licenseId: string;\n userId: string;\n signal?: AbortSignal;\n}\n\nexport interface PortalServerActionDefinition<Input, Output> {\n id: string;\n description: string;\n visibility: PortalActionVisibility;\n tags: string[];\n run: (input: Input, context?: PortalActionContext) => Promise<Output>;\n}\n\nexport const defineServerAction = <Input, Output>(\n definition: PortalServerActionDefinition<Input, Output>\n) => definition;\n\nexport interface CreateServiceAccountInput {\n token: string;\n name: string;\n}\n\nexport interface ServiceAccountData {\n id: string;\n customerId: string;\n token: string;\n accountName: string;\n isRevoked: boolean;\n createdAt: string;\n emailAddress: string;\n}\n\nexport interface CreateServiceAccountResult {\n service_account: ServiceAccountData;\n token: string;\n}\n\nexport const createServiceAccount = defineServerAction<\n CreateServiceAccountInput,\n CreateServiceAccountResult\n>({\n id: \"service-account/create\",\n description: \"Creates a service account for installing applications\",\n visibility: \"customer\",\n tags: [\"service-account\", \"install\"],\n async run({ token, name }) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Service account creation requires a session token\");\n }\n\n if (!name || typeof name !== \"string\" || !name.trim()) {\n throw new Error(\"Service account name is required\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/service-account`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\n \"[portal-components] creating service account via %s\",\n endpoint\n );\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"POST\",\n token,\n headers: {\n \"content-type\": \"application/json\"\n },\n body: JSON.stringify({ account_name: name.trim() })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Service account creation failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n const data: CreateServiceAccountResult = await response.json();\n return data;\n }\n});\n\nexport interface InitiateLoginInput {\n email: string;\n}\n\nexport interface InitiateLoginResult {\n status: \"ok\" | \"saml_redirect\";\n requestedAt: string;\n message: string;\n /** If SAML redirect is required, this contains the info needed to redirect */\n saml?: {\n redirectRequired: true;\n customerId: string;\n email: string;\n appSlug: string;\n };\n}\n\n/**\n * Reference server action for initiating the passwordless login flow.\n * Real portals should replace the simulated delay with a call to their auth API.\n */\nexport const initiateLogin = defineServerAction<\n InitiateLoginInput,\n InitiateLoginResult\n>({\n id: \"auth/initiate-login\",\n description:\n \"Begins the passwordless login flow by dispatching a magic link email.\",\n visibility: \"customer\",\n tags: [\"auth\", \"login\", \"session\"],\n async run(input) {\n const endpoint = `${getApiOrigin()}/v3/login/magic-link`;\n const appSlug = process.env.PORTAL_APP_SLUG;\n if (!appSlug) {\n throw new Error(\"PORTAL_APP_SLUG is not configured\");\n }\n const portalOrigin =\n process.env.PORTAL_ORIGIN ?? \"https://enterprise.replicated.com\";\n const redirectUri = `${portalOrigin.replace(/\\/+$/, \"\")}/${appSlug}/login`;\n\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\"\n },\n body: JSON.stringify({\n app_slug: appSlug,\n email_address: input.email,\n redirect_uri: redirectUri\n })\n });\n\n if (!response.ok) {\n throw new Error(\n `Magic link request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n\n // Check if SAML redirect is required\n if (data.saml_redirect_required && data.saml_customer_id) {\n return {\n status: \"saml_redirect\",\n requestedAt: new Date().toISOString(),\n message: \"SAML authentication required\",\n saml: {\n redirectRequired: true,\n customerId: data.saml_customer_id,\n email: input.email,\n appSlug\n }\n };\n }\n\n return {\n status: \"ok\",\n requestedAt: new Date().toISOString(),\n message: `Magic link requested for ${input.email}`\n };\n }\n});\n\nexport interface VerifyMagicLinkInput {\n nonce: string;\n}\n\nexport interface VerifyMagicLinkResult {\n token: string;\n raw: unknown;\n}\n\nexport interface VerifyMagicLinkError {\n code: \"invalid_code\" | \"expired\" | \"unknown\";\n message: string;\n isExpired?: boolean;\n}\n\nexport const verifyMagicLink = defineServerAction<\n VerifyMagicLinkInput,\n VerifyMagicLinkResult\n>({\n id: \"auth/verify-magic-link\",\n description: \"Verifies the 12-digit code provided via email and returns a JWT.\",\n visibility: \"customer\",\n tags: [\"auth\", \"login\", \"verify\"],\n async run({ nonce }) {\n const endpoint = `${getApiOrigin()}/v3/login/magic-link/verify`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\n \"[portal-components] verifying magic link via %s\",\n endpoint\n );\n }\n\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\"\n },\n body: JSON.stringify({ nonce })\n });\n\n if (!response.ok) {\n if (response.status === 401) {\n // Check if the response indicates an expired link\n try {\n const errorBody = await response.json();\n if (errorBody?.is_expired === true) {\n const error: VerifyMagicLinkError = {\n code: \"expired\",\n message: \"Magic link has expired. A new link has been sent to your email.\",\n isExpired: true\n };\n throw error;\n }\n } catch (parseError) {\n // If we already threw an error, re-throw it\n if ((parseError as VerifyMagicLinkError)?.code === \"expired\") {\n throw parseError;\n }\n // Otherwise fall through to invalid_code\n }\n\n const error: VerifyMagicLinkError = {\n code: \"invalid_code\",\n message: \"Incorrect code, check your email and try again.\"\n };\n throw error;\n }\n const error: VerifyMagicLinkError = {\n code: \"unknown\",\n message: `Magic link verification failed (${response.status} ${response.statusText})`\n };\n throw error;\n }\n\n const payload = await response.json();\n const token = payload?.token ?? payload?.jwt ?? payload?.access_token;\n if (typeof token !== \"string\") {\n throw new Error(\"Magic link verification succeeded but no token returned\");\n }\n\n return { token, raw: payload };\n }\n});\n\nexport interface CustomBrandingResponse {\n brandingData: string;\n documentation: unknown;\n}\n\nexport interface PortalLicenseField {\n key: string;\n label: string;\n value: string | null;\n isSecret?: boolean;\n}\n\nexport interface PortalLicenseDetails {\n id?: string;\n status?: string;\n statusLabel?: string;\n environment?: string;\n expiresAt?: string | null;\n releaseChannels?: string[];\n installMethods?: string[];\n installNotes?: string;\n customerName?: string;\n customerId?: string;\n customerOrganization?: string;\n fields: PortalLicenseField[];\n}\n\nexport interface ListSupportBundlesInput {\n token: string;\n}\n\nexport interface SupportBundleInsight {\n level: string;\n primary: string;\n key?: string;\n detail?: string;\n}\n\nexport interface SupportBundleSummary {\n id: string;\n createdAt?: string;\n status?: string;\n size?: number;\n instanceId?: string;\n insights?: SupportBundleInsight[];\n metadata?: Record<string, unknown>;\n}\n\nexport interface ListSupportBundlesResult {\n bundles: SupportBundleSummary[];\n totalCount: number;\n raw: unknown;\n}\n\nexport interface DownloadSupportBundleInput {\n token: string;\n bundleId: string;\n}\n\nexport interface DownloadSupportBundleResult {\n signedUrl: string;\n}\n\nexport interface DeleteSupportBundleInput {\n token: string;\n bundleId: string;\n}\n\nexport interface DeleteSupportBundleResult {\n success: boolean;\n}\n\nexport interface UploadSupportBundleInput {\n token: string;\n appId: string;\n}\n\nexport interface UploadSupportBundleResult {\n uploadUrl: string;\n appId: string;\n}\n\nexport interface UploadSupportBundleCompleteInput {\n token: string;\n appId: string;\n fileContent: ArrayBuffer;\n contentLength: number;\n}\n\nexport interface UploadSupportBundleCompleteResult {\n bundleId: string;\n slug: string;\n}\n\nexport interface FetchLicenseDetailsInput {\n token: string;\n}\n\nexport interface FetchLicenseDetailsResult {\n license: PortalLicenseDetails;\n raw: unknown;\n}\n\nexport interface FetchInstallOptionsInput {\n token: string;\n}\n\nexport interface FetchInstallOptionsResult {\n showLinux: boolean;\n showHelm: boolean;\n}\n\nexport interface FetchLicenseSummaryInput {\n token: string;\n}\n\nexport interface FetchLicenseSummaryResult {\n type: string;\n expiresAt: string | null;\n}\n\nexport interface FetchCustomersInput {\n token: string;\n}\n\nexport interface Customer {\n id: string;\n name: string;\n licenseId: string;\n licenseType: string;\n expiresAt: string;\n isEnterprisePortalEnabled: boolean;\n}\n\nexport interface FetchCustomersResult {\n customers: Customer[];\n}\n\nexport interface SwitchCustomerInput {\n token: string;\n customerId: string;\n}\n\nexport interface SwitchCustomerResult {\n token: string;\n}\n\nexport interface ListReleasesInput {\n token: string;\n}\n\nexport interface ListReleasesResult {\n status: number;\n body: string | null;\n}\n\n/**\n * Internal implementation of fetchCustomBranding.\n * Wrapped with React's cache() to deduplicate calls within a single request.\n */\nconst fetchCustomBrandingImpl = async (): Promise<CustomBrandingResponse> => {\n const appSlug = process.env.PORTAL_APP_SLUG;\n\n if (!appSlug) {\n throw new Error(\"PORTAL_APP_SLUG is not configured\");\n }\n\n const url = `${getApiOrigin()}/v3/custom-branding?app_slug=${encodeURIComponent(\n appSlug\n )}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\n \"[portal-components] fetching custom branding via %s\",\n url\n );\n }\n\n const response = await fetch(url, {\n headers: {\n accept: \"application/json\"\n }\n });\n\n if (!response.ok) {\n throw new Error(\n `Custom branding request failed (${response.status} ${response.statusText})`\n );\n }\n\n const payload = await response.json();\n const brandingData = payload?.branding_data;\n\n if (typeof brandingData !== \"string\") {\n throw new Error(\"Custom branding response missing branding_data string\");\n }\n\n return {\n brandingData,\n documentation: payload?.documentation ?? null\n };\n};\n\n/**\n * Fetches custom branding for the portal.\n * This function is cached per-request to avoid duplicate API calls when called\n * from multiple components (e.g., TopNav and page components).\n */\nexport const fetchCustomBranding = cache(fetchCustomBrandingImpl);\n\nexport const decodeJwtPayload = (token: string): Record<string, unknown> => {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n throw new Error(\"Invalid JWT received\");\n }\n\n const payloadSegment = parts[1];\n if (!payloadSegment) {\n throw new Error(\"JWT payload segment missing\");\n }\n\n const padded = payloadSegment.padEnd(\n payloadSegment.length + ((4 - (payloadSegment.length % 4)) % 4),\n \"=\"\n );\n const decoded = Buffer.from(padded, \"base64\").toString(\"utf-8\");\n return JSON.parse(decoded) as Record<string, unknown>;\n};\n\n/**\n * Extracts customer ID from JWT token. Throws if extraction fails.\n */\nexport const getCustomerIdFromToken = (token: string): string => {\n const payload = decodeJwtPayload(token);\n const customerId = payload?.customer_id || payload?.customerId;\n if (typeof customerId !== \"string\" || !customerId.trim()) {\n throw new Error(\"Unable to determine customer_id from session token\");\n }\n return customerId.trim();\n};\n\nconst resolveSupportBundlesEndpoint = () => {\n const fallback = `${getApiOrigin()}/v3/supportbundles`;\n const explicit = process.env.SUPPORT_BUNDLES_ENDPOINT;\n\n if (!explicit) {\n return new URL(fallback);\n }\n\n try {\n return new URL(explicit);\n } catch (error) {\n console.warn(\n `[portal-components] invalid SUPPORT_BUNDLES_ENDPOINT, using fallback`,\n error\n );\n return new URL(fallback);\n }\n};\n\nexport const listSupportBundles = defineServerAction<\n ListSupportBundlesInput,\n ListSupportBundlesResult\n>({\n id: \"support/list-bundles\",\n description:\n \"Fetches support bundles associated with the customer found in the portal session JWT.\",\n visibility: \"customer\",\n tags: [\"support\", \"bundles\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Support bundle listing requires a session token\");\n }\n\n const payload = decodeJwtPayload(token);\n const customerId = payload?.customer_id;\n if (typeof customerId !== \"string\" || !customerId.trim()) {\n throw new Error(\"Unable to determine customer_id from session token\");\n }\n\n const url = resolveSupportBundlesEndpoint();\n url.searchParams.set(\"customer_id\", customerId.trim());\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching support bundles via %s\", url);\n }\n\n const response = await authenticatedFetch(url.toString(), {\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (context?.signal?.aborted) {\n throw new Error(\"Support bundles request was aborted\");\n }\n\n if (!response.ok) {\n throw new Error(\n `Support bundles request failed (${response.status} ${response.statusText})`\n );\n }\n\n const raw = await response.json();\n\n const rawRecord =\n raw && typeof raw === \"object\" ? (raw as Record<string, unknown>) : undefined;\n\n const parseInsights = (raw: unknown): SupportBundleInsight[] | undefined => {\n if (!Array.isArray(raw)) return undefined;\n return raw\n .filter((i): i is Record<string, unknown> => i && typeof i === \"object\")\n .map((i) => ({\n level: String(i.level ?? \"\"),\n primary: String(i.primary ?? \"\"),\n key: typeof i.key === \"string\" ? i.key : undefined,\n detail: typeof i.detail === \"string\" ? i.detail : undefined\n }));\n };\n\n const bundles: SupportBundleSummary[] = Array.isArray(\n rawRecord?.supportBundles\n )\n ? (rawRecord?.supportBundles as unknown[]).map((item) => {\n if (!item || typeof item !== \"object\") {\n return {\n id: \"\",\n createdAt: undefined,\n status: undefined,\n size: undefined,\n instanceId: undefined,\n insights: undefined,\n metadata: undefined\n };\n }\n\n const record = item as Record<string, unknown>;\n return {\n id: String(record.id ?? \"\"),\n createdAt:\n typeof record.createdAt === \"string\"\n ? (record.createdAt as string)\n : undefined,\n status:\n typeof record.status === \"string\"\n ? (record.status as string)\n : undefined,\n size:\n typeof record.size === \"number\"\n ? record.size\n : undefined,\n instanceId:\n typeof record.instanceId === \"string\"\n ? record.instanceId\n : undefined,\n insights: parseInsights(record.insights),\n metadata: record\n };\n })\n : Array.isArray(raw)\n ? (raw as unknown[]).map((item) => {\n if (!item || typeof item !== \"object\") {\n return {\n id: \"\",\n createdAt: undefined,\n status: undefined,\n size: undefined,\n instanceId: undefined,\n insights: undefined,\n metadata: undefined\n };\n }\n const record = item as Record<string, unknown>;\n return {\n id: String(record.id ?? \"\"),\n createdAt:\n typeof record.createdAt === \"string\"\n ? (record.createdAt as string)\n : undefined,\n status:\n typeof record.status === \"string\"\n ? (record.status as string)\n : undefined,\n size:\n typeof record.size === \"number\"\n ? record.size\n : undefined,\n instanceId:\n typeof record.instanceId === \"string\"\n ? record.instanceId\n : undefined,\n insights: parseInsights(record.insights),\n metadata: record\n };\n })\n : [];\n\n const totalCount = (() => {\n if (rawRecord) {\n if (\n typeof rawRecord.totalCount === \"number\" &&\n Number.isFinite(rawRecord.totalCount)\n ) {\n return rawRecord.totalCount;\n }\n if (Array.isArray(rawRecord.supportBundles)) {\n return rawRecord.supportBundles.length;\n }\n }\n\n if (Array.isArray(raw)) {\n return raw.length;\n }\n\n return bundles.length;\n })();\n\n return {\n bundles,\n totalCount,\n raw\n };\n }\n});\n\nexport const downloadSupportBundle = defineServerAction<\n DownloadSupportBundleInput,\n DownloadSupportBundleResult\n>({\n id: \"support/download-bundle\",\n description: \"Gets a signed URL for downloading a support bundle.\",\n visibility: \"customer\",\n tags: [\"support\", \"bundles\", \"download\"],\n async run({ token, bundleId }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Support bundle download requires a session token\");\n }\n\n if (!bundleId || typeof bundleId !== \"string\") {\n throw new Error(\"Support bundle download requires a bundle ID\");\n }\n\n const payload = decodeJwtPayload(token);\n const customerId = payload?.customer_id;\n if (typeof customerId !== \"string\" || !customerId.trim()) {\n throw new Error(\"Unable to determine customer_id from session token\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/supportbundle/${encodeURIComponent(bundleId)}/download?customer_id=${encodeURIComponent(customerId.trim())}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] getting support bundle download URL via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n throw new Error(\n `Support bundle download URL request failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n const data = await response.json();\n const signedUrl = data?.signedUrl;\n\n if (typeof signedUrl !== \"string\" || !signedUrl) {\n throw new Error(\"Support bundle download response missing signedUrl\");\n }\n\n return { signedUrl };\n }\n});\n\nexport const deleteSupportBundle = defineServerAction<\n DeleteSupportBundleInput,\n DeleteSupportBundleResult\n>({\n id: \"support/delete-bundle\",\n description: \"Deletes a support bundle.\",\n visibility: \"customer\",\n tags: [\"support\", \"bundles\", \"delete\"],\n async run({ token, bundleId }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Support bundle deletion requires a session token\");\n }\n\n if (!bundleId || typeof bundleId !== \"string\") {\n throw new Error(\"Support bundle deletion requires a bundle ID\");\n }\n\n const payload = decodeJwtPayload(token);\n const customerId = payload?.customer_id;\n if (typeof customerId !== \"string\" || !customerId.trim()) {\n throw new Error(\"Unable to determine customer_id from session token\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/supportbundle/${encodeURIComponent(bundleId)}?customer_id=${encodeURIComponent(customerId.trim())}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] deleting support bundle via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"DELETE\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n if (response.status === 404) {\n throw new Error(\"Support bundle not found\");\n }\n throw new Error(\n `Support bundle deletion failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n return { success: true };\n }\n});\n\nexport const uploadSupportBundle = defineServerAction<\n UploadSupportBundleCompleteInput,\n UploadSupportBundleCompleteResult\n>({\n id: \"support/upload-bundle\",\n description: \"Uploads a support bundle to the server.\",\n visibility: \"customer\",\n tags: [\"support\", \"bundles\", \"upload\"],\n async run({ token, appId, fileContent, contentLength }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Support bundle upload requires a session token\");\n }\n\n if (!appId || typeof appId !== \"string\") {\n throw new Error(\"Support bundle upload requires an app ID\");\n }\n\n if (!fileContent || !(fileContent instanceof ArrayBuffer)) {\n throw new Error(\"Support bundle upload requires file content\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/supportbundle/upload/${encodeURIComponent(appId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] uploading support bundle via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"POST\",\n token,\n headers: {\n \"content-type\": \"application/gzip\",\n \"content-length\": String(contentLength)\n },\n body: fileContent,\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n throw new Error(\n `Support bundle upload failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n const data = await response.json();\n\n return {\n bundleId: data?.bundleId ?? data?.bundle_id ?? \"\",\n slug: data?.slug ?? \"\"\n };\n }\n});\n\n/**\n * Helper to get the upload endpoint URL for client-side uploads with progress tracking.\n * Use this when you need progress indication - call this to get the URL, then upload directly from client.\n */\nexport const getSupportBundleUploadUrl = (appId: string): string => {\n return `${getApiOrigin()}/v3/supportbundle/upload/${encodeURIComponent(appId)}`;\n};\n\nexport const listReleases = defineServerAction<\n ListReleasesInput,\n ListReleasesResult\n>({\n id: \"releases/list\",\n description: \"Lists available releases for the authenticated customer.\",\n visibility: \"customer\",\n tags: [\"releases\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"List releases requires a session token\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/release-history`;\n\n console.log(\"[portal-components] listReleases request\", {\n endpoint\n });\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n const bodyText = await response\n .text()\n .catch((error) => {\n console.warn(\"[portal-components] listReleases read error\", error);\n return null;\n });\n\n console.log(\"[portal-components] listReleases response\", response.status, bodyText);\n\n if (!response.ok) {\n throw new Error(\n `List releases request failed (${response.status} ${response.statusText})`\n );\n }\n\n return {\n status: response.status,\n body: bodyText\n };\n }\n});\n\nconst asRecord = (value: unknown): Record<string, unknown> | undefined => {\n if (value && typeof value === \"object\") {\n return value as Record<string, unknown>;\n }\n return undefined;\n};\n\nconst getValue = (\n record: Record<string, unknown> | undefined,\n key: string\n) => (record ? record[key] : undefined);\n\nconst getString = (\n record: Record<string, unknown> | undefined,\n key: string\n): string | undefined => {\n const value = getValue(record, key);\n return typeof value === \"string\" ? value : undefined;\n};\n\nconst getBoolean = (\n record: Record<string, unknown> | undefined,\n key: string\n): boolean | undefined => {\n const value = getValue(record, key);\n if (typeof value === \"boolean\") {\n return value;\n }\n if (typeof value === \"number\") {\n return value === 1;\n }\n if (typeof value === \"string\") {\n const normalized = value.trim().toLowerCase();\n if ([\"true\", \"1\", \"yes\"].includes(normalized)) {\n return true;\n }\n if ([\"false\", \"0\", \"no\"].includes(normalized)) {\n return false;\n }\n }\n return undefined;\n};\n\nconst toDisplayValue = (value: unknown): string | null => {\n if (value === null || value === undefined) {\n return null;\n }\n if (typeof value === \"string\") {\n return value;\n }\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n};\n\nconst normalizeStringArray = (value: unknown): string[] | undefined => {\n if (Array.isArray(value)) {\n const normalized = value\n .map((item) =>\n typeof item === \"string\" ? item.trim() : \"\"\n )\n .filter((item) => item.length > 0);\n return normalized.length ? normalized : undefined;\n }\n\n if (typeof value === \"string\") {\n const normalized = value\n .split(\",\")\n .map((item) => item.trim())\n .filter((item) => item.length > 0);\n return normalized.length ? normalized : undefined;\n }\n\n return undefined;\n};\n\nconst normalizeLicenseFields = (input: unknown): PortalLicenseField[] => {\n if (!input) {\n return [];\n }\n\n if (Array.isArray(input)) {\n return input\n .map((field, index) => {\n if (!field || typeof field !== \"object\") {\n return null;\n }\n const candidate = field as Record<string, unknown>;\n const key =\n typeof candidate.key === \"string\" && candidate.key.trim().length\n ? candidate.key.trim()\n : typeof candidate.name === \"string\" && candidate.name.trim().length\n ? candidate.name.trim()\n : typeof candidate.label === \"string\" && candidate.label.trim().length\n ? candidate.label.trim()\n : `field-${index}`;\n const label =\n typeof candidate.label === \"string\" && candidate.label.trim().length\n ? candidate.label.trim()\n : typeof candidate.name === \"string\" && candidate.name.trim().length\n ? candidate.name.trim()\n : key;\n let value: unknown =\n candidate.value ?? candidate.data ?? candidate.content;\n if (\n (value === undefined || value === null) &&\n typeof candidate.text === \"string\"\n ) {\n value = candidate.text;\n }\n if (\n (value === undefined || value === null) &&\n typeof candidate.defaultValue === \"string\"\n ) {\n value = candidate.defaultValue;\n }\n const isSecret = Boolean(\n candidate.isSecret ?? candidate.secret ?? candidate.masked\n );\n const resolved = toDisplayValue(value);\n return {\n key,\n label,\n value: resolved,\n isSecret\n } as PortalLicenseField;\n })\n .filter((field): field is PortalLicenseField => Boolean(field));\n }\n\n if (typeof input === \"object\") {\n return Object.entries(input as Record<string, unknown>).map(\n ([key, value]) => {\n let resolvedValue: unknown = value;\n let isSecret = false;\n if (value && typeof value === \"object\") {\n const obj = value as Record<string, unknown>;\n if (\"value\" in obj) {\n resolvedValue = obj.value;\n }\n isSecret = Boolean(obj.isSecret ?? obj.secret ?? obj.masked);\n }\n const normalized = toDisplayValue(resolvedValue);\n return {\n key,\n label: key,\n value: normalized,\n isSecret\n };\n }\n );\n }\n\n return [];\n};\n\nconst extractChannelNames = (input: unknown): string[] | undefined => {\n if (!Array.isArray(input)) {\n return undefined;\n }\n const names = input\n .map((item) => {\n if (typeof item === \"string\") {\n return item.trim();\n }\n const record = asRecord(item);\n if (!record) {\n return null;\n }\n return (\n getString(record, \"name\") ??\n getString(record, \"channelName\") ??\n getString(record, \"channel\") ??\n getString(record, \"channelSlug\") ??\n getString(record, \"slug\") ??\n undefined\n );\n })\n .filter((name): name is string => Boolean(name && name.length));\n return names.length ? names : undefined;\n};\n\nconst normalizeEntitlementFields = (\n fieldsInput: unknown,\n valuesInput: unknown\n): PortalLicenseField[] => {\n const valuesMap = new Map<string, string | null>();\n const assignValue = (key: string | undefined, value: unknown) => {\n if (!key) {\n return;\n }\n valuesMap.set(key, toDisplayValue(value));\n };\n\n if (Array.isArray(valuesInput)) {\n valuesInput.forEach((item) => {\n const record = asRecord(item);\n if (!record) {\n if (typeof item === \"string\") {\n assignValue(item, item);\n }\n return;\n }\n const key =\n getString(record, \"name\") ??\n getString(record, \"field\") ??\n getString(record, \"title\") ??\n getString(record, \"label\") ??\n getString(record, \"slug\") ??\n (() => {\n const idValue = getValue(record, \"id\");\n if (typeof idValue === \"string\" || typeof idValue === \"number\") {\n return String(idValue);\n }\n return undefined;\n })();\n const value =\n getValue(record, \"value\") ??\n getValue(record, \"currentValue\") ??\n getValue(record, \"entitlementValue\") ??\n getValue(record, \"content\") ??\n getValue(record, \"data\") ??\n getValue(record, \"defaultVal\") ??\n getValue(record, \"defaultValue\");\n assignValue(key, value);\n });\n } else if (valuesInput && typeof valuesInput === \"object\") {\n Object.entries(valuesInput as Record<string, unknown>).forEach(\n ([key, value]) => assignValue(key, value)\n );\n }\n\n const normalized: PortalLicenseField[] = [];\n\n if (Array.isArray(fieldsInput)) {\n fieldsInput.forEach((item, index) => {\n const record = asRecord(item);\n if (!record) {\n return;\n }\n const baseKey =\n getString(record, \"name\") ??\n getString(record, \"field\") ??\n getString(record, \"slug\") ??\n `entitlement-${index}`;\n const key = `entitlement-${baseKey}`;\n const label =\n getString(record, \"title\") ??\n getString(record, \"label\") ??\n baseKey;\n const defaultValue =\n getString(record, \"defaultVal\") ??\n getString(record, \"default\") ??\n getString(record, \"defaultValue\");\n const value =\n valuesMap.get(baseKey) ??\n valuesMap.get(label) ??\n defaultValue ??\n null;\n const isSecret = Boolean(\n getBoolean(record, \"secret\") ??\n getBoolean(record, \"isSecret\") ??\n getBoolean(record, \"masked\")\n );\n normalized.push({\n key,\n label,\n value,\n isSecret\n });\n });\n }\n\n valuesMap.forEach((value, key) => {\n const normalizedKey = `entitlement-${key}`;\n if (!normalized.some((field) => field.key === normalizedKey)) {\n normalized.push({\n key: normalizedKey,\n label: key,\n value\n });\n }\n });\n\n return normalized;\n};\n\nconst normalizeLicensePayload = (payload: unknown): PortalLicenseDetails => {\n const payloadRecord = asRecord(payload);\n const rootRecord =\n asRecord(getValue(payloadRecord, \"license\")) ??\n asRecord(getValue(payloadRecord, \"data\")) ??\n payloadRecord ??\n ({} as Record<string, unknown>);\n const sourceRecord =\n asRecord(getValue(rootRecord, \"metadata\")) ?? rootRecord;\n\n const customer =\n asRecord(getValue(rootRecord, \"customer\")) ??\n asRecord(getValue(sourceRecord, \"customer\")) ??\n asRecord(getValue(payloadRecord, \"customer\")) ??\n ({} as Record<string, unknown>);\n\n let releaseChannels =\n normalizeStringArray(\n getValue(rootRecord, \"releaseChannels\") ??\n getValue(sourceRecord, \"releaseChannels\") ??\n getValue(sourceRecord, \"channels\") ??\n getValue(rootRecord, \"channels\") ??\n getValue(sourceRecord, \"channel\") ??\n getValue(rootRecord, \"channel\")\n ) ?? undefined;\n\n if (!releaseChannels) {\n releaseChannels =\n extractChannelNames(getValue(rootRecord, \"channels\")) ??\n extractChannelNames(getValue(sourceRecord, \"channels\")) ??\n undefined;\n }\n\n let installMethods =\n normalizeStringArray(\n getValue(rootRecord, \"installMethods\") ??\n getValue(sourceRecord, \"installMethods\") ??\n getValue(sourceRecord, \"install_options\") ??\n getValue(rootRecord, \"install_options\") ??\n getValue(sourceRecord, \"installOptions\")\n ) ?? undefined;\n\n if (!installMethods || installMethods.length === 0) {\n const resolved: string[] = [];\n const flag = (key: string) =>\n getBoolean(rootRecord, key) ?? getBoolean(sourceRecord, key) ?? false;\n\n if (flag(\"isKotsInstallEnabled\")) {\n resolved.push(\"Replicated KOTS\");\n }\n if (flag(\"isHelmInstallEnabled\")) {\n resolved.push(\"Helm\");\n }\n if (flag(\"isHelmAirgapEnabled\")) {\n resolved.push(\"Helm Airgap\");\n }\n if (\n flag(\"isEmbeddedClusterDownloadEnabled\") ||\n flag(\"isEmbeddedClusterMultiNodeEnabled\")\n ) {\n resolved.push(\"Embedded Cluster\");\n }\n if (flag(\"isKurlInstallEnabled\")) {\n resolved.push(\"kURL\");\n }\n if (flag(\"isGitopsSupported\")) {\n resolved.push(\"GitOps\");\n }\n if (resolved.length) {\n installMethods = Array.from(new Set(resolved));\n }\n }\n\n const expiresAtSource =\n getValue(sourceRecord, \"expiresAt\") ??\n getValue(sourceRecord, \"expireAt\") ??\n getValue(sourceRecord, \"expire_at\") ??\n getValue(sourceRecord, \"expiration\") ??\n getValue(sourceRecord, \"expirationDate\") ??\n getValue(sourceRecord, \"expires_on\") ??\n getValue(rootRecord, \"expiresAt\") ??\n getValue(rootRecord, \"expireAt\") ??\n getValue(rootRecord, \"expire_at\") ??\n getValue(rootRecord, \"expiration\");\n\n const expiresAt =\n typeof expiresAtSource === \"string\" && expiresAtSource.trim().length\n ? expiresAtSource\n : expiresAtSource === null\n ? null\n : undefined;\n\n const baseFields = normalizeLicenseFields(\n getValue(rootRecord, \"additionalFields\") ??\n getValue(sourceRecord, \"additionalFields\") ??\n getValue(sourceRecord, \"fields\") ??\n getValue(rootRecord, \"fields\") ??\n getValue(payloadRecord, \"fields\") ??\n getValue(payloadRecord, \"additional_fields\")\n );\n\n const entitlementFields = normalizeEntitlementFields(\n getValue(rootRecord, \"entitlementFields\") ??\n getValue(sourceRecord, \"entitlementFields\"),\n getValue(rootRecord, \"entitlementValues\") ??\n getValue(sourceRecord, \"entitlementValues\")\n );\n\n const fields = [\n ...baseFields,\n ...entitlementFields.filter(\n (field) => !baseFields.some((existing) => existing.key === field.key)\n )\n ];\n\n const statusFromSource =\n getString(sourceRecord, \"status\") ??\n getString(sourceRecord, \"state\");\n const statusLabelFromSource =\n getString(sourceRecord, \"statusLabel\") ??\n getString(sourceRecord, \"stateLabel\");\n const expiredFlag =\n getBoolean(sourceRecord, \"isExpired\") ??\n getBoolean(rootRecord, \"isExpired\");\n const derivedStatus =\n statusFromSource ??\n (typeof expiredFlag === \"boolean\"\n ? expiredFlag\n ? \"expired\"\n : \"active\"\n : undefined);\n const statusLabel =\n statusLabelFromSource ??\n (derivedStatus\n ? derivedStatus.charAt(0).toUpperCase() + derivedStatus.slice(1)\n : undefined);\n\n const licenseType =\n getString(sourceRecord, \"licenseType\") ??\n getString(rootRecord, \"licenseType\");\n\n const status = derivedStatus;\n\n const license: PortalLicenseDetails = {\n id:\n getString(rootRecord, \"id\") ??\n getString(sourceRecord, \"id\") ??\n getString(sourceRecord, \"licenseId\") ??\n getString(customer, \"licenseId\") ??\n undefined,\n status,\n statusLabel,\n environment:\n getString(sourceRecord, \"environment\") ??\n getString(sourceRecord, \"tier\") ??\n licenseType ??\n undefined,\n expiresAt: expiresAt ?? null,\n releaseChannels: releaseChannels ?? [\n getString(rootRecord, \"channelName\") ??\n getString(rootRecord, \"channel\") ??\n undefined\n ].filter((value): value is string => Boolean(value)),\n installMethods,\n installNotes: getString(sourceRecord, \"installNotes\"),\n customerName:\n getString(sourceRecord, \"customerName\") ??\n getString(customer, \"name\") ??\n undefined,\n customerId:\n getString(sourceRecord, \"customerId\") ??\n getString(customer, \"id\") ??\n getString(rootRecord, \"customerId\") ??\n undefined,\n customerOrganization:\n getString(customer, \"organization\") ??\n getString(sourceRecord, \"customerOrganization\") ??\n getString(rootRecord, \"customerOrganization\") ??\n undefined,\n fields\n };\n\n return license;\n};\n\nexport const fetchLicenseDetails = defineServerAction<\n FetchLicenseDetailsInput,\n FetchLicenseDetailsResult\n>({\n id: \"license/fetch-details\",\n description: \"Fetches the authenticated user's enterprise license details.\",\n visibility: \"customer\",\n tags: [\"license\", \"entitlements\"],\n async run({ token }, context) {\n if (typeof token !== \"string\" || token.trim().length === 0) {\n throw new Error(\"fetchLicenseDetails requires a non-empty token\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/license`;\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `License request failed (${response.status} ${response.statusText})`\n );\n }\n\n const payload = await response.json();\n const license = normalizeLicensePayload(payload);\n\n return {\n license,\n raw: payload ?? null\n };\n }\n});\n\nexport const fetchInstallOptions = defineServerAction<\n FetchInstallOptionsInput,\n FetchInstallOptionsResult\n>({\n id: \"license/fetch-install-options\",\n description: \"Fetches install options based on license entitlements.\",\n visibility: \"customer\",\n tags: [\"license\", \"install\"],\n async run({ token }, context) {\n if (typeof token !== \"string\" || token.trim().length === 0) {\n throw new Error(\"fetchInstallOptions requires a non-empty token\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/license`;\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `License request failed (${response.status} ${response.statusText})`\n );\n }\n\n const payload = await response.json();\n \n // Check for embedded cluster (Linux) and Helm install flags\n const getBoolean = (obj: unknown, key: string): boolean => {\n if (obj && typeof obj === \"object\" && key in obj) {\n const val = (obj as Record<string, unknown>)[key];\n return val === true || val === \"true\";\n }\n return false;\n };\n\n const license = payload?.license ?? payload ?? {};\n const showLinux = getBoolean(license, \"isEmbeddedClusterDownloadEnabled\");\n const showHelm = getBoolean(license, \"isHelmInstallEnabled\");\n\n return {\n showLinux,\n showHelm\n };\n }\n});\n\nexport const fetchLicenseSummary = defineServerAction<\n FetchLicenseSummaryInput,\n FetchLicenseSummaryResult\n>({\n id: \"license/fetch-summary\",\n description: \"Fetches license summary for the license card.\",\n visibility: \"customer\",\n tags: [\"license\"],\n async run({ token }, context) {\n if (typeof token !== \"string\" || token.trim().length === 0) {\n throw new Error(\"fetchLicenseSummary requires a non-empty token\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/license`;\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `License request failed (${response.status} ${response.statusText})`\n );\n }\n\n const payload = await response.json();\n const license = normalizeLicensePayload(payload);\n\n // Extract type and expiration\n const type = license.environment || \"Unknown\";\n const expiresAt = license.expiresAt || null;\n\n return {\n type,\n expiresAt\n };\n }\n});\n\nexport const fetchCustomers = defineServerAction<\n FetchCustomersInput,\n FetchCustomersResult\n>({\n id: \"auth/fetch-customers\",\n description: \"Fetches the list of customers/teams for the authenticated user.\",\n visibility: \"customer\",\n tags: [\"auth\", \"customers\"],\n async run({ token }, context) {\n if (typeof token !== \"string\" || token.trim().length === 0) {\n throw new Error(\"fetchCustomers requires a non-empty token\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/customers`;\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Fetch customers request failed (${response.status} ${response.statusText})`\n );\n }\n\n const payload = await response.json();\n \n return {\n customers: payload.customers || []\n };\n }\n});\n\nexport const switchCustomer = defineServerAction<\n SwitchCustomerInput,\n SwitchCustomerResult\n>({\n id: \"auth/switch-customer\",\n description: \"Switches the JWT to a different customer/team.\",\n visibility: \"customer\",\n tags: [\"auth\", \"customers\"],\n async run({ token, customerId }, context) {\n if (typeof token !== \"string\" || token.trim().length === 0) {\n throw new Error(\"switchCustomer requires a non-empty token\");\n }\n \n if (typeof customerId !== \"string\" || customerId.trim().length === 0) {\n throw new Error(\"switchCustomer requires a non-empty customerId\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/select-customer`;\n\n const requestBody = { customer_id: customerId };\n\n const response = await authenticatedFetch(endpoint, {\n method: \"PUT\",\n token,\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify(requestBody),\n signal: context?.signal\n });\n\n console.log('[portal-components] switchCustomer response status:', response.status);\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('[portal-components] switchCustomer error response:', errorText);\n throw new Error(\n `Switch customer request failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n const payload = await response.json();\n console.log('[portal-components] switchCustomer response payload:', payload);\n \n // API returns 'jwt' field, not 'token'\n const newToken = payload.jwt || payload.token || token;\n console.log('[portal-components] switchCustomer using token field:', payload.jwt ? 'jwt' : (payload.token ? 'token' : 'fallback'));\n \n return {\n token: newToken\n };\n }\n});\n\n// =============================================================================\n// Security Types\n// =============================================================================\n\nexport type SecurityInstallType = \"linux\" | \"helm\";\n\nexport interface SecurityScanSummary {\n critical: Record<string, string>;\n high: Record<string, string>;\n medium: Record<string, string>;\n low: Record<string, string>;\n}\n\nexport interface SecurityScanWrapper {\n input: string;\n digest?: string;\n last_scanned_at?: string;\n result: SecurityScanSummary;\n not_found?: boolean;\n}\n\nexport interface SecurityReleaseImage {\n image: string;\n sha: string;\n size: number;\n platforms: { os: string; architecture: string }[];\n security?: SecurityScanWrapper;\n}\n\nexport interface GetSecurityInfoInput {\n token: string;\n installType: SecurityInstallType;\n channelSequence: number;\n isAirgap?: boolean;\n}\n\nexport interface GetSecurityInfoResult {\n images: SecurityReleaseImage[];\n}\n\nexport interface SecurityInfoDiff {\n oldTags: string[];\n newTags: string[];\n oldVulns?: SecurityScanSummary;\n newVulns?: SecurityScanSummary;\n added?: SecurityScanSummary;\n removed?: SecurityScanSummary;\n}\n\nexport interface GetSecurityInfoDiffInput {\n token: string;\n installType: SecurityInstallType;\n fromChannelSequence: number;\n toChannelSequence: number;\n isAirgap?: boolean;\n}\n\nexport interface GetSecurityInfoDiffResult {\n from_channel_sequence: number;\n to_channel_sequence: number;\n images: Record<string, SecurityInfoDiff>;\n}\n\nexport interface UnifiedSbom {\n sbom: string;\n sbom_source: string;\n}\n\nexport interface SpdxCreationInfo {\n created: string;\n creators: string[];\n}\n\nexport interface SpdxDocument {\n SPDXID: string;\n spdxVersion: string;\n name: string;\n creationInfo?: SpdxCreationInfo;\n packages?: unknown[];\n files?: unknown[];\n}\n\nexport interface GetSecurityInfoSBOMInput {\n token: string;\n installType: SecurityInstallType;\n channelSequence: number;\n isAirgap?: boolean;\n unifiedSbom?: boolean;\n}\n\nexport interface GetSecurityInfoSBOMResult {\n sboms: {\n unified?: UnifiedSbom;\n };\n}\n\n// =============================================================================\n// Security Actions\n// =============================================================================\n\n/**\n * Fetches security scan (CVE) information for a specific release.\n */\nexport const getSecurityInfo = defineServerAction<\n GetSecurityInfoInput,\n GetSecurityInfoResult\n>({\n id: \"security/get-info\",\n description: \"Fetches CVE security scan results for a specific release\",\n visibility: \"customer\",\n tags: [\"security\", \"cve\"],\n async run({ token, installType, channelSequence, isAirgap = false }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Security info request requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n\n const params = new URLSearchParams({\n customer_id: customerId,\n install_type: installType,\n channel_sequence: channelSequence.toString(),\n is_airgap: isAirgap.toString()\n });\n\n const url = `${getApiOrigin()}/v3/security-info?${params.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching security info via %s\", url);\n }\n\n const response = await authenticatedFetch(url, {\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Security info request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n return data as GetSecurityInfoResult;\n }\n});\n\n/**\n * Fetches security diff between two releases (fixed/added CVEs).\n */\nexport const getSecurityInfoDiff = defineServerAction<\n GetSecurityInfoDiffInput,\n GetSecurityInfoDiffResult\n>({\n id: \"security/get-info-diff\",\n description: \"Fetches CVE diff between two releases showing fixed and added vulnerabilities\",\n visibility: \"customer\",\n tags: [\"security\", \"cve\", \"diff\"],\n async run({ token, installType, fromChannelSequence, toChannelSequence, isAirgap = false }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Security info diff request requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n\n const params = new URLSearchParams({\n customer_id: customerId,\n install_type: installType,\n from_channel_sequence: fromChannelSequence.toString(),\n to_channel_sequence: toChannelSequence.toString(),\n is_airgap: isAirgap.toString()\n });\n\n const url = `${getApiOrigin()}/v3/security-info-diff?${params.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching security info diff via %s\", url);\n }\n\n const response = await authenticatedFetch(url, {\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Security info diff request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n return data as GetSecurityInfoDiffResult;\n }\n});\n\n/**\n * Fetches SBOM (Software Bill of Materials) for a specific release.\n */\nexport const getSecurityInfoSBOM = defineServerAction<\n GetSecurityInfoSBOMInput,\n GetSecurityInfoSBOMResult\n>({\n id: \"security/get-sbom\",\n description: \"Fetches Software Bill of Materials (SBOM) for a specific release\",\n visibility: \"customer\",\n tags: [\"security\", \"sbom\"],\n async run({ token, installType, channelSequence, isAirgap = false, unifiedSbom = true }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Security SBOM request requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n\n const params = new URLSearchParams({\n customer_id: customerId,\n install_type: installType,\n channel_sequence: channelSequence.toString(),\n is_airgap: isAirgap.toString(),\n unified_sbom: unifiedSbom.toString()\n });\n\n const url = `${getApiOrigin()}/v3/security-info-sbom?${params.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching security SBOM via %s\", url);\n }\n\n const response = await authenticatedFetch(url, {\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n // Handle 204 No Content response\n if (response.status === 204) {\n return { sboms: {} };\n }\n\n if (!response.ok) {\n throw new Error(\n `Security SBOM request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n return data as GetSecurityInfoSBOMResult;\n }\n});\n\n// =============================================================================\n// Dashboard Types\n// =============================================================================\n\nexport interface FetchTeamStatsInput {\n token: string;\n}\n\nexport interface TeamUser {\n id: string;\n email: string;\n name?: string;\n createdAt?: string;\n}\n\nexport interface ServiceAccountSummary {\n id: string;\n accountName: string;\n customerId: string;\n isRevoked: boolean;\n createdAt: string;\n}\n\nexport interface FetchTeamStatsResult {\n userCount: number;\n serviceAccountCount: number;\n}\n\nexport interface FetchDashboardInstancesInput {\n token: string;\n}\n\nexport interface FetchDashboardInstancesResult {\n onlineActiveCount: number;\n airgapCount: number;\n onlineUpdates: number;\n airgapUpdates: number;\n}\n\n// =============================================================================\n// Dashboard Actions\n// =============================================================================\n\n/**\n * Fetches team statistics including user count and service account count.\n * Used by the Team Settings dashboard card.\n */\nexport const fetchTeamStats = defineServerAction<\n FetchTeamStatsInput,\n FetchTeamStatsResult\n>({\n id: \"dashboard/fetch-team-stats\",\n description: \"Fetches user and service account counts for the dashboard\",\n visibility: \"customer\",\n tags: [\"dashboard\", \"team\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Team stats request requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const origin = getApiOrigin();\n\n // Fetch users count\n let userCount = 0;\n try {\n const usersUrl = `${origin}/v3/users?exclude_invites=false&customer_id=${encodeURIComponent(customerId)}`;\n \n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching team users via %s\", usersUrl);\n }\n\n const usersResponse = await authenticatedFetch(usersUrl, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (usersResponse.ok) {\n const usersData = await usersResponse.json();\n userCount = Array.isArray(usersData.users) ? usersData.users.length : 0;\n }\n } catch (error) {\n console.error(\"[portal-components] Error fetching users:\", error);\n }\n\n // Fetch service accounts count\n let serviceAccountCount = 0;\n try {\n const saUrl = `${origin}/v3/service-accounts?customer_id=${encodeURIComponent(customerId)}`;\n \n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching service accounts via %s\", saUrl);\n }\n\n const saResponse = await authenticatedFetch(saUrl, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (saResponse.ok) {\n const saData = await saResponse.json();\n serviceAccountCount = Array.isArray(saData.serviceAccounts) \n ? saData.serviceAccounts.length \n : 0;\n }\n } catch (error) {\n console.error(\"[portal-components] Error fetching service accounts:\", error);\n }\n\n return {\n userCount,\n serviceAccountCount\n };\n }\n});\n\n/**\n * Fetches instance counts and available updates for the dashboard.\n * Used by the Updates dashboard card.\n */\nexport const fetchDashboardInstances = defineServerAction<\n FetchDashboardInstancesInput,\n FetchDashboardInstancesResult\n>({\n id: \"dashboard/fetch-instances\",\n description: \"Fetches instance counts and update availability for the dashboard\",\n visibility: \"customer\",\n tags: [\"dashboard\", \"instances\", \"updates\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Dashboard instances request requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const origin = getApiOrigin();\n\n // Fetch instances\n const instancesUrl = `${origin}/v3/instances?customer_id=${encodeURIComponent(customerId)}`;\n \n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching instances via %s\", instancesUrl);\n }\n\n const instancesResponse = await authenticatedFetch(instancesUrl, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!instancesResponse.ok) {\n throw new Error(\n `Instances request failed (${instancesResponse.status} ${instancesResponse.statusText})`\n );\n }\n\n const instancesData = await instancesResponse.json();\n const allInstances = instancesData.instances || [];\n\n // Split into online and airgap\n const onlineInstances = allInstances.filter((i: { isAirgap?: boolean }) => !i.isAirgap);\n const airgapInstances = allInstances.filter((i: { isAirgap?: boolean }) => i.isAirgap);\n\n // Filter to active online instances (checked in within 24 hours)\n const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1000;\n const activeOnlineInstances = onlineInstances.filter((instance: { lastCheckin?: string }) => {\n const lastCheckin = instance.lastCheckin \n ? new Date(instance.lastCheckin).getTime() \n : 0;\n return lastCheckin > twentyFourHoursAgo;\n });\n\n const onlineActiveCount = activeOnlineInstances.length;\n const airgapCount = airgapInstances.length;\n\n // Fetch channel releases to calculate updates\n let channelReleases: Array<{ channelId: string; channelSequence: number }> = [];\n try {\n const releasesUrl = `${origin}/v3/channel-releases?customer_id=${encodeURIComponent(customerId)}`;\n \n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching channel releases via %s\", releasesUrl);\n }\n\n const releasesResponse = await authenticatedFetch(releasesUrl, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (releasesResponse.ok) {\n const releasesData = await releasesResponse.json();\n channelReleases = releasesData.channelReleases || [];\n }\n } catch (error) {\n console.error(\"[portal-components] Error fetching channel releases:\", error);\n }\n\n // Calculate updates for active online instances\n const calculateUpdates = (instances: Array<{ channelId?: string; channelSequence?: number }>) => {\n if (!channelReleases.length) return 0;\n \n let numUpdates = 0;\n for (const instance of instances) {\n const instanceSequence = instance.channelSequence ?? 0;\n const matchingReleases = channelReleases.filter(\n (release) => release.channelId === instance.channelId\n );\n for (const release of matchingReleases) {\n if (release.channelSequence > instanceSequence) {\n numUpdates++;\n }\n }\n }\n return numUpdates;\n };\n\n const onlineUpdates = calculateUpdates(activeOnlineInstances);\n const airgapUpdates = calculateUpdates(airgapInstances);\n\n return {\n onlineActiveCount,\n airgapCount,\n onlineUpdates,\n airgapUpdates\n };\n }\n});\n\n// =============================================================================\n// User Settings Types\n// =============================================================================\n\nexport interface FetchCurrentUserInput {\n token: string;\n}\n\nexport interface UserProfile {\n emailAddress: string;\n firstName: string;\n lastName: string;\n}\n\nexport interface FetchCurrentUserResult {\n user: UserProfile;\n}\n\nexport interface UpdateUserInput {\n token: string;\n firstName?: string;\n lastName?: string;\n}\n\nexport interface UpdateUserResult {\n success: boolean;\n}\n\nexport interface NotificationSetting {\n type: string;\n enabled: boolean;\n}\n\nexport interface FetchNotificationsInput {\n token: string;\n customerId: string;\n}\n\nexport interface FetchNotificationsResult {\n notifications: NotificationSetting[];\n}\n\nexport interface UpdateNotificationsInput {\n token: string;\n customerId: string;\n notifications: NotificationSetting[];\n}\n\nexport interface UpdateNotificationsResult {\n notifications: NotificationSetting[];\n}\n\n// =============================================================================\n// User Settings Actions\n// =============================================================================\n\n/**\n * Fetches the current user's profile information.\n */\nexport const fetchCurrentUser = defineServerAction<\n FetchCurrentUserInput,\n FetchCurrentUserResult\n>({\n id: \"user/fetch-current\",\n description: \"Fetches the current user's profile information\",\n visibility: \"customer\",\n tags: [\"user\", \"profile\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Fetch current user requires a session token\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/user`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching current user via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Fetch current user request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n \n return {\n user: {\n emailAddress: data.emailAddress || \"\",\n firstName: data.firstName || \"\",\n lastName: data.lastName || \"\"\n }\n };\n }\n});\n\n/**\n * Updates the current user's profile information.\n */\nexport const updateUser = defineServerAction<\n UpdateUserInput,\n UpdateUserResult\n>({\n id: \"user/update\",\n description: \"Updates the current user's first and/or last name\",\n visibility: \"customer\",\n tags: [\"user\", \"profile\"],\n async run({ token, firstName, lastName }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Update user requires a session token\");\n }\n\n if (!firstName && !lastName) {\n throw new Error(\"At least one of firstName or lastName must be provided\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/user`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] updating user via %s\", endpoint);\n }\n\n const body: { firstName?: string; lastName?: string } = {};\n if (firstName !== undefined) body.firstName = firstName;\n if (lastName !== undefined) body.lastName = lastName;\n\n const response = await authenticatedFetch(endpoint, {\n method: \"POST\",\n token,\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify(body),\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n throw new Error(\n `Update user request failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n return { success: true };\n }\n});\n\n/**\n * Fetches notification preferences for a specific customer/team.\n */\nexport const fetchNotifications = defineServerAction<\n FetchNotificationsInput,\n FetchNotificationsResult\n>({\n id: \"notifications/fetch\",\n description: \"Fetches notification preferences for a specific team\",\n visibility: \"customer\",\n tags: [\"notifications\", \"user\"],\n async run({ token, customerId }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Fetch notifications requires a session token\");\n }\n\n if (!customerId || typeof customerId !== \"string\") {\n throw new Error(\"Fetch notifications requires a customerId\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/notifications?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching notifications via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Fetch notifications request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n \n return {\n notifications: data.notifications || []\n };\n }\n});\n\n/**\n * Updates notification preferences for a specific customer/team.\n */\nexport const updateNotifications = defineServerAction<\n UpdateNotificationsInput,\n UpdateNotificationsResult\n>({\n id: \"notifications/update\",\n description: \"Updates notification preferences for a specific team\",\n visibility: \"customer\",\n tags: [\"notifications\", \"user\"],\n async run({ token, customerId, notifications }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Update notifications requires a session token\");\n }\n\n if (!customerId || typeof customerId !== \"string\") {\n throw new Error(\"Update notifications requires a customerId\");\n }\n\n if (!Array.isArray(notifications)) {\n throw new Error(\"Update notifications requires a notifications array\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/notifications?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] updating notifications via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"PUT\",\n token,\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify({ notifications }),\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n throw new Error(\n `Update notifications request failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n const data = await response.json();\n \n return {\n notifications: data.notifications || []\n };\n }\n});\n\n// =============================================================================\n// Team Settings Types\n// =============================================================================\n\nexport interface TeamUser {\n emailAddress: string;\n firstAccessedAt?: string;\n lastAccessedAt?: string;\n viewCount?: number;\n pendingInvite?: boolean;\n}\n\nexport interface FetchTeamUsersInput {\n token: string;\n limit?: number;\n offset?: number;\n}\n\nexport interface FetchTeamUsersResult {\n users: TeamUser[];\n total: number;\n}\n\nexport interface InviteUserInput {\n token: string;\n email: string;\n}\n\nexport interface InviteUserResult {\n success: boolean;\n}\n\nexport interface DeleteUserInput {\n token: string;\n email: string;\n}\n\nexport interface DeleteUserResult {\n success: boolean;\n}\n\nexport interface ServiceAccount {\n id: string;\n customerId: string;\n accountName: string;\n emailAddress?: string;\n token: string;\n isRevoked: boolean;\n createdAt: string;\n lastUsedAt?: string;\n tokenRegeneratedAt?: string;\n}\n\nexport interface FetchServiceAccountsInput {\n token: string;\n limit?: number;\n offset?: number;\n includeRevoked?: boolean;\n}\n\nexport interface FetchServiceAccountsResult {\n serviceAccounts: ServiceAccount[];\n total: number;\n}\n\nexport interface RevokeServiceAccountInput {\n token: string;\n accountId: string;\n}\n\nexport interface RevokeServiceAccountResult {\n success: boolean;\n}\n\nexport interface RotateServiceAccountTokenInput {\n token: string;\n accountId: string;\n}\n\nexport interface RotateServiceAccountTokenResult {\n serviceAccount: ServiceAccount;\n helmLoginCommand: string;\n redeployHelm: string[];\n}\n\nexport interface Instance {\n id: string;\n serviceAccountId?: string;\n versionLabel?: string;\n channelId?: string;\n channelSequence?: number;\n lastCheckin?: string;\n isAirgap?: boolean;\n embeddedClusterVersion?: string;\n tags?: Array<{ key: string; value: string }>;\n}\n\nexport interface FetchInstancesInput {\n token: string;\n}\n\nexport interface FetchInstancesResult {\n instances: Instance[];\n}\n\nexport interface SAMLConfig {\n samlAllowed: boolean;\n samlEnabled: boolean;\n entityId: string;\n acsUrl: string;\n hasIdpMetadata: boolean;\n hasIdpCert: boolean;\n}\n\nexport interface FetchSamlConfigInput {\n token: string;\n}\n\nexport interface FetchSamlConfigResult {\n config: SAMLConfig;\n}\n\nexport interface UpdateSamlConfigInput {\n token: string;\n idpMetadataXml: string;\n idpPublicCert: string;\n}\n\nexport interface UpdateSamlConfigResult {\n success: boolean;\n}\n\nexport interface ToggleSamlEnabledInput {\n token: string;\n enabled: boolean;\n}\n\nexport interface ToggleSamlEnabledResult {\n success: boolean;\n samlEnabled: boolean;\n}\n\nexport interface DeprovisionSamlInput {\n token: string;\n}\n\nexport interface DeprovisionSamlResult {\n success: boolean;\n}\n\n// =============================================================================\n// Team Settings Actions\n// =============================================================================\n\n/**\n * Fetches the list of users for a team.\n */\nexport const fetchTeamUsers = defineServerAction<\n FetchTeamUsersInput,\n FetchTeamUsersResult\n>({\n id: \"team/fetch-users\",\n description: \"Fetches paginated list of team users and pending invites\",\n visibility: \"customer\",\n tags: [\"team\", \"users\"],\n async run({ token, limit = 25, offset = 0 }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Fetch team users requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const params = new URLSearchParams({\n customer_id: customerId,\n limit: limit.toString(),\n offset: offset.toString()\n });\n\n const endpoint = `${getApiOrigin()}/v3/users?${params.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching team users via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Fetch team users request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n \n return {\n users: data.users || [],\n total: data.total || 0\n };\n }\n});\n\n/**\n * Invites a user to the team.\n */\nexport const inviteUser = defineServerAction<\n InviteUserInput,\n InviteUserResult\n>({\n id: \"team/invite-user\",\n description: \"Sends an invitation email to join the team\",\n visibility: \"customer\",\n tags: [\"team\", \"users\", \"invite\"],\n async run({ token, email }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Invite user requires a session token\");\n }\n\n if (!email || typeof email !== \"string\") {\n throw new Error(\"Invite user requires an email address\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const params = new URLSearchParams({\n customer_id: customerId,\n email_address: email\n });\n\n const endpoint = `${getApiOrigin()}/v3/invite?${params.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] inviting user via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"POST\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n let errorMessage = \"Failed to invite user\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n return { success: true };\n }\n});\n\n/**\n * Removes a user from the team.\n */\nexport const deleteUser = defineServerAction<\n DeleteUserInput,\n DeleteUserResult\n>({\n id: \"team/delete-user\",\n description: \"Removes a user from the team\",\n visibility: \"customer\",\n tags: [\"team\", \"users\", \"delete\"],\n async run({ token, email }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Delete user requires a session token\");\n }\n\n if (!email || typeof email !== \"string\") {\n throw new Error(\"Delete user requires an email address\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const params = new URLSearchParams({\n customer_id: customerId,\n email_address: email\n });\n\n const endpoint = `${getApiOrigin()}/v3/user?${params.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] deleting user via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"DELETE\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n let errorMessage = \"Failed to delete user\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n return { success: true };\n }\n});\n\n/**\n * Fetches the list of service accounts for a team.\n */\nexport const fetchServiceAccounts = defineServerAction<\n FetchServiceAccountsInput,\n FetchServiceAccountsResult\n>({\n id: \"team/fetch-service-accounts\",\n description: \"Fetches paginated list of service accounts\",\n visibility: \"customer\",\n tags: [\"team\", \"service-accounts\"],\n async run({ token, limit = 50, offset = 0, includeRevoked = false }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Fetch service accounts requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const params = new URLSearchParams({\n customer_id: customerId,\n limit: limit.toString(),\n offset: offset.toString()\n });\n\n // Add filterRevoked parameter - API filters revoked when this param is present\n if (!includeRevoked) {\n params.set(\"filterRevoked\", \"false\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/service-accounts?${params.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching service accounts via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Fetch service accounts request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n \n return {\n serviceAccounts: data.serviceAccounts || [],\n total: data.total || 0\n };\n }\n});\n\n/**\n * Revokes a service account.\n */\nexport const revokeServiceAccount = defineServerAction<\n RevokeServiceAccountInput,\n RevokeServiceAccountResult\n>({\n id: \"team/revoke-service-account\",\n description: \"Revokes a service account (soft delete)\",\n visibility: \"customer\",\n tags: [\"team\", \"service-accounts\", \"revoke\"],\n async run({ token, accountId }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Revoke service account requires a session token\");\n }\n\n if (!accountId || typeof accountId !== \"string\") {\n throw new Error(\"Revoke service account requires an account ID\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/v3/service-account/${encodeURIComponent(accountId)}?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] revoking service account via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"DELETE\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n let errorMessage = \"Failed to revoke service account\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n return { success: true };\n }\n});\n\n/**\n * Rotates a service account token.\n */\nexport const rotateServiceAccountToken = defineServerAction<\n RotateServiceAccountTokenInput,\n RotateServiceAccountTokenResult\n>({\n id: \"team/rotate-service-account-token\",\n description: \"Generates a new token for a service account\",\n visibility: \"customer\",\n tags: [\"team\", \"service-accounts\", \"rotate\"],\n async run({ token, accountId }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Rotate service account token requires a session token\");\n }\n\n if (!accountId || typeof accountId !== \"string\") {\n throw new Error(\"Rotate service account token requires an account ID\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/v3/service-account/${encodeURIComponent(accountId)}/rotate-token?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] rotating service account token via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"POST\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n let errorMessage = \"Failed to rotate service account token\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n const data = await response.json();\n \n return {\n serviceAccount: data.service_account,\n helmLoginCommand: data.helm_login_cmd || \"\",\n redeployHelm: data.redeploy_helm || []\n };\n }\n});\n\n/**\n * Fetches instances for the customer.\n */\nexport const fetchInstances = defineServerAction<\n FetchInstancesInput,\n FetchInstancesResult\n>({\n id: \"team/fetch-instances\",\n description: \"Fetches instances to determine service account usage\",\n visibility: \"customer\",\n tags: [\"team\", \"instances\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Fetch instances requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/v3/instances?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching instances via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Fetch instances request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n \n return {\n instances: data.instances || []\n };\n }\n});\n\n/**\n * Fetches SAML configuration for the customer.\n */\nexport const fetchSamlConfig = defineServerAction<\n FetchSamlConfigInput,\n FetchSamlConfigResult\n>({\n id: \"team/fetch-saml-config\",\n description: \"Fetches SAML SSO configuration for the team\",\n visibility: \"customer\",\n tags: [\"team\", \"saml\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Fetch SAML config requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/v3/customer/saml/config?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching SAML config via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Fetch SAML config request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n \n return {\n config: {\n samlAllowed: data.samlAllowed || false,\n samlEnabled: data.samlEnabled || false,\n entityId: data.entityId || \"\",\n acsUrl: data.acsUrl || \"\",\n hasIdpMetadata: data.hasIdpMetadata || false,\n hasIdpCert: data.hasIdpCert || false\n }\n };\n }\n});\n\n/**\n * Updates SAML configuration (uploads IdP metadata and certificate).\n */\nexport const updateSamlConfig = defineServerAction<\n UpdateSamlConfigInput,\n UpdateSamlConfigResult\n>({\n id: \"team/update-saml-config\",\n description: \"Uploads IdP metadata and certificate for SAML SSO\",\n visibility: \"customer\",\n tags: [\"team\", \"saml\", \"update\"],\n async run({ token, idpMetadataXml, idpPublicCert }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Update SAML config requires a session token\");\n }\n\n if (!idpMetadataXml || !idpPublicCert) {\n throw new Error(\"Both IdP metadata and certificate are required\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/v3/customer/saml/config?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] updating SAML config via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"PUT\",\n token,\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify({\n idpMetadataXml,\n idpPublicCert\n }),\n signal: context?.signal\n });\n\n if (!response.ok) {\n let errorMessage = \"Failed to update SAML configuration\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n return { success: true };\n }\n});\n\n/**\n * Toggles SAML authentication enabled/disabled.\n */\nexport const toggleSamlEnabled = defineServerAction<\n ToggleSamlEnabledInput,\n ToggleSamlEnabledResult\n>({\n id: \"team/toggle-saml-enabled\",\n description: \"Enables or disables SAML authentication\",\n visibility: \"customer\",\n tags: [\"team\", \"saml\", \"toggle\"],\n async run({ token, enabled }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Toggle SAML enabled requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/v3/customer/saml/enable?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] toggling SAML enabled via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"PUT\",\n token,\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify({ enabled }),\n signal: context?.signal\n });\n\n if (!response.ok) {\n let errorMessage = \"Failed to toggle SAML\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n const data = await response.json();\n \n return {\n success: true,\n samlEnabled: data.samlEnabled || enabled\n };\n }\n});\n\n/**\n * Removes SAML configuration (deprovisions SAML).\n */\nexport const deprovisionSaml = defineServerAction<\n DeprovisionSamlInput,\n DeprovisionSamlResult\n>({\n id: \"team/deprovision-saml\",\n description: \"Removes all SAML configuration\",\n visibility: \"customer\",\n tags: [\"team\", \"saml\", \"delete\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Deprovision SAML requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/v3/customer/saml/config?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] deprovisioning SAML via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"DELETE\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n let errorMessage = \"Failed to remove SAML configuration\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n return { success: true };\n }\n});\n\n// =============================================================================\n// Invite Accept/Refresh Actions\n// =============================================================================\n\nexport interface AcceptInviteInput {\n code: string;\n}\n\nexport interface AcceptInviteResult {\n token: string;\n}\n\nexport interface AcceptInviteError {\n code: \"invalid_code\" | \"expired\" | \"unknown\";\n message: string;\n}\n\n/**\n * Accepts a team invitation using the invite code from email.\n * Returns a JWT token on success that can be used to establish a session.\n */\nexport const acceptInvite = defineServerAction<\n AcceptInviteInput,\n AcceptInviteResult\n>({\n id: \"auth/accept-invite\",\n description: \"Accepts a team invitation and returns a session token\",\n visibility: \"customer\",\n tags: [\"auth\", \"invite\", \"join\"],\n async run({ code }) {\n if (!code || typeof code !== \"string\") {\n const error: AcceptInviteError = {\n code: \"invalid_code\",\n message: \"Invite code is required\"\n };\n throw error;\n }\n\n const endpoint = `${getApiOrigin()}/v3/invite/accept`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] accepting invite via %s\", endpoint);\n }\n\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify({ code })\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n const error: AcceptInviteError = {\n code: \"invalid_code\",\n message: \"Invalid or expired invite code. Please check your code and try again.\"\n };\n throw error;\n }\n\n let errorMessage = \"Failed to accept invitation\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n\n const error: AcceptInviteError = {\n code: \"unknown\",\n message: errorMessage\n };\n throw error;\n }\n\n const payload = await response.json();\n const token = payload?.jwt ?? payload?.token;\n\n if (typeof token !== \"string\") {\n throw new Error(\"Invite accepted but no token returned\");\n }\n\n return { token };\n }\n});\n\nexport interface RefreshInviteInput {\n code: string;\n}\n\nexport interface RefreshInviteResult {\n success: boolean;\n}\n\n/**\n * Refreshes an expired invite by generating a new code and resending the email.\n * The original code is used to identify the invite to refresh.\n */\nexport const refreshInvite = defineServerAction<\n RefreshInviteInput,\n RefreshInviteResult\n>({\n id: \"auth/refresh-invite\",\n description: \"Refreshes an expired invite and resends the invitation email\",\n visibility: \"customer\",\n tags: [\"auth\", \"invite\", \"refresh\"],\n async run({ code }) {\n if (!code || typeof code !== \"string\") {\n throw new Error(\"Invite code is required\");\n }\n\n const endpoint = `${getApiOrigin()}/v3/invite/refresh`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] refreshing invite via %s\", endpoint);\n }\n\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify({ code })\n });\n\n // The API returns 200 even for non-existent codes to prevent code enumeration\n if (!response.ok) {\n let errorMessage = \"Failed to refresh invitation\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n return { success: true };\n }\n});\n","/**\n * Install-related server actions for the Linux and Helm installation wizards.\n * \n * These actions handle the complete installation flow including:\n * - Fetching available releases (channel releases)\n * - Creating/updating/fetching install options\n * - Generating installation instructions\n */\n\nimport { authenticatedFetch } from \"../utils/api-client\";\nimport { getApiOrigin, getCustomerIdFromToken } from \"./index\";\nimport type { PortalActionContext } from \"./index\";\n\n// =============================================================================\n// Types - Channel Releases\n// =============================================================================\n\nexport interface EmbeddedClusterInstallationType {\n version?: string;\n}\n\nexport interface HelmInstallationType {\n version?: string;\n}\n\nexport interface InstallationTypes {\n embeddedCluster?: EmbeddedClusterInstallationType;\n helm?: HelmInstallationType;\n}\n\nexport interface ChannelRelease {\n channelId: string;\n channelName: string;\n channelSlug: string;\n channelSequence: number;\n releaseSequence: number;\n versionLabel: string;\n releaseNotes?: string;\n createdAt: string;\n isRequired?: boolean;\n installationTypes?: InstallationTypes;\n}\n\nexport interface FetchChannelReleasesInput {\n token: string;\n channelId?: string;\n}\n\nexport interface FetchChannelReleasesResult {\n channelReleases: ChannelRelease[];\n}\n\n// =============================================================================\n// Types - Install Options\n// =============================================================================\n\nexport type NetworkAvailability = \"online\" | \"proxy\" | \"airgap\";\nexport type InstallType = \"linux\" | \"helm\";\nexport type RegistryAvailability = \"online\" | \"partial\" | \"offline\";\nexport type KubernetesDistribution = \"vanilla\" | \"openshift\" | \"rancher\" | \"aks\" | \"eks\" | \"gke\";\nexport type InstallStatus = \"in_progress\" | \"completed\" | \"discarded\";\n\nexport interface InstallStep {\n step_number: number;\n step_name: string;\n title: string;\n description?: string;\n commands: string[];\n /** If true, this step can be marked as completed (shows checkmark UI) */\n maybe_completed?: boolean;\n}\n\nexport interface InstallInstructions {\n format: \"basic\" | \"mdx\";\n install_type: string;\n title: string;\n steps?: InstallStep[];\n mdx_template?: string;\n context?: Record<string, unknown>;\n renderer_version?: number;\n}\n\nexport interface InstallOptions {\n id: string;\n customer_id: string;\n install_type: InstallType;\n instance_name: string;\n instance_id?: string;\n service_account_id?: string;\n service_account_email_address?: string;\n license_id?: string;\n started_at: string;\n completed_at?: string;\n network_availability?: NetworkAvailability;\n is_multi_node?: boolean;\n channel_id?: string;\n channel_release_sequence?: number;\n registry_availability?: RegistryAvailability;\n kubernetes_distribution?: KubernetesDistribution;\n admin_console_url?: string;\n status?: InstallStatus;\n}\n\nexport interface CreateInstallOptionsInput {\n token: string;\n installType: InstallType;\n instanceName: string;\n serviceAccountId: string;\n networkAvailability: NetworkAvailability;\n isMultiNode?: boolean;\n channelId?: string;\n channelReleaseSequence?: number;\n // Helm-specific\n registryAvailability?: RegistryAvailability;\n kubernetesDistribution?: KubernetesDistribution;\n}\n\nexport interface CreateInstallOptionsResult {\n install_options: InstallOptions;\n instructions?: InstallInstructions;\n}\n\nexport interface GetInstallOptionsInput {\n token: string;\n installOptionsId: string;\n includeInstructions?: boolean;\n privateRegistryHostname?: string;\n proxyUrl?: string;\n}\n\nexport interface GetInstallOptionsResult {\n id: string;\n customer_id: string;\n install_type: InstallType;\n instance_name: string;\n instance_id?: string;\n service_account_id?: string;\n service_account_email_address?: string;\n started_at: string;\n completed_at?: string;\n network_availability?: NetworkAvailability;\n is_multi_node?: boolean;\n channel_id?: string;\n channel_release_sequence?: number;\n registry_availability?: RegistryAvailability;\n kubernetes_distribution?: KubernetesDistribution;\n admin_console_url?: string;\n status?: InstallStatus;\n instructions?: InstallInstructions;\n /** Timestamp when assets were downloaded (for progress tracking) */\n assets_downloaded_at?: string;\n /** Timestamp when registry authentication was completed (for Helm progress tracking) */\n registry_authenticated_at?: string;\n /** Timestamp when images were pulled (for Helm progress tracking) */\n images_pulled_at?: string;\n /** Timestamp when installation was completed (for progress tracking) */\n installation_completed_at?: string;\n}\n\nexport interface UpdateInstallOptionsInput {\n token: string;\n installOptionsId: string;\n installType?: InstallType;\n channelId?: string;\n channelReleaseSequence?: number;\n networkAvailability?: NetworkAvailability;\n registryAvailability?: RegistryAvailability;\n kubernetesDistribution?: KubernetesDistribution;\n isMultiNode?: boolean;\n serviceAccountId?: string;\n adminConsoleUrl?: string | null;\n status?: InstallStatus;\n includeInstructions?: boolean;\n privateRegistryHostname?: string;\n proxyUrl?: string;\n}\n\nexport interface UpdateInstallOptionsResult {\n install_options: InstallOptions;\n instructions?: InstallInstructions;\n}\n\n// =============================================================================\n// Actions - Channel Releases\n// =============================================================================\n\n/**\n * Fetches available channel releases for the customer.\n * These are filtered to show only releases that have embedded cluster installers.\n */\nexport async function fetchChannelReleases(\n input: FetchChannelReleasesInput,\n context?: PortalActionContext\n): Promise<FetchChannelReleasesResult> {\n const { token, channelId } = input;\n\n if (!token || typeof token !== \"string\") {\n throw new Error(\"fetchChannelReleases requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const origin = getApiOrigin();\n\n const url = new URL(`${origin}/v3/channel-releases`);\n url.searchParams.set(\"customer_id\", customerId);\n if (channelId) {\n url.searchParams.set(\"channel_id\", channelId);\n }\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching channel releases via %s\", url.toString());\n }\n\n const response = await authenticatedFetch(url.toString(), {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Channel releases request failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n const payload = await response.json();\n \n return {\n channelReleases: payload.channelReleases || []\n };\n}\n\n// =============================================================================\n// Actions - Install Options\n// =============================================================================\n\n/**\n * Creates a new install options record to track an installation attempt.\n * This is called after creating a service account and before showing install commands.\n */\nexport async function createInstallOptions(\n input: CreateInstallOptionsInput,\n context?: PortalActionContext\n): Promise<CreateInstallOptionsResult> {\n const {\n token,\n installType,\n instanceName,\n serviceAccountId,\n networkAvailability,\n isMultiNode = false,\n channelId,\n channelReleaseSequence,\n registryAvailability,\n kubernetesDistribution\n } = input;\n\n if (!token || typeof token !== \"string\") {\n throw new Error(\"createInstallOptions requires a session token\");\n }\n\n if (!instanceName?.trim()) {\n throw new Error(\"Instance name is required\");\n }\n\n if (!serviceAccountId?.trim()) {\n throw new Error(\"Service account ID is required\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const origin = getApiOrigin();\n const endpoint = `${origin}/v3/customers/${customerId}/install-options?includeInstructions=true`;\n\n const body: Record<string, unknown> = {\n install_type: installType,\n instance_name: instanceName.trim(),\n service_account_id: serviceAccountId.trim(),\n network_availability: networkAvailability,\n is_multi_node: isMultiNode\n };\n\n // Add optional fields\n if (channelId) {\n body.channel_id = channelId;\n }\n if (channelReleaseSequence !== undefined) {\n body.channel_release_sequence = channelReleaseSequence;\n }\n if (registryAvailability) {\n body.registry_availability = registryAvailability;\n }\n if (kubernetesDistribution) {\n body.kubernetes_distribution = kubernetesDistribution;\n }\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] creating install options via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"POST\",\n token,\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify(body),\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Create install options failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n return await response.json();\n}\n\n/**\n * Fetches existing install options by ID.\n * Can optionally include generated installation instructions.\n */\nexport async function getInstallOptions(\n input: GetInstallOptionsInput,\n context?: PortalActionContext\n): Promise<GetInstallOptionsResult> {\n const {\n token,\n installOptionsId,\n includeInstructions = true,\n privateRegistryHostname,\n proxyUrl\n } = input;\n\n if (!token || typeof token !== \"string\") {\n throw new Error(\"getInstallOptions requires a session token\");\n }\n\n if (!installOptionsId?.trim()) {\n throw new Error(\"Install options ID is required\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const origin = getApiOrigin();\n\n const url = new URL(`${origin}/v3/customers/${customerId}/install-options/${installOptionsId.trim()}`);\n \n if (includeInstructions) {\n url.searchParams.set(\"includeInstructions\", \"true\");\n }\n if (privateRegistryHostname) {\n url.searchParams.set(\"privateRegistryHostname\", privateRegistryHostname);\n }\n if (proxyUrl) {\n url.searchParams.set(\"proxyUrl\", proxyUrl);\n }\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching install options via %s\", url.toString());\n }\n\n const response = await authenticatedFetch(url.toString(), {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Get install options failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n return await response.json();\n}\n\n/**\n * Updates existing install options.\n * Typically called when user selects a different version or changes settings.\n * Returns updated options with regenerated installation instructions.\n */\nexport async function updateInstallOptions(\n input: UpdateInstallOptionsInput,\n context?: PortalActionContext\n): Promise<UpdateInstallOptionsResult> {\n const {\n token,\n installOptionsId,\n installType,\n channelId,\n channelReleaseSequence,\n networkAvailability,\n registryAvailability,\n kubernetesDistribution,\n isMultiNode,\n serviceAccountId,\n adminConsoleUrl,\n status,\n includeInstructions = true,\n privateRegistryHostname,\n proxyUrl\n } = input;\n\n if (!token || typeof token !== \"string\") {\n throw new Error(\"updateInstallOptions requires a session token\");\n }\n\n if (!installOptionsId?.trim()) {\n throw new Error(\"Install options ID is required\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const origin = getApiOrigin();\n\n const url = new URL(`${origin}/v3/customers/${customerId}/install-options/${installOptionsId.trim()}`);\n \n if (includeInstructions) {\n url.searchParams.set(\"includeInstructions\", \"true\");\n }\n if (privateRegistryHostname) {\n url.searchParams.set(\"privateRegistryHostname\", privateRegistryHostname);\n }\n if (proxyUrl) {\n url.searchParams.set(\"proxyUrl\", proxyUrl);\n }\n\n // Build body with only provided fields\n const body: Record<string, unknown> = {};\n\n if (installType !== undefined) {\n body.install_type = installType;\n }\n if (channelId !== undefined) {\n body.channel_id = channelId;\n }\n if (channelReleaseSequence !== undefined) {\n body.channel_release_sequence = channelReleaseSequence;\n }\n if (networkAvailability !== undefined) {\n body.network_availability = networkAvailability;\n }\n if (registryAvailability !== undefined) {\n body.registry_availability = registryAvailability;\n }\n if (kubernetesDistribution !== undefined) {\n body.kubernetes_distribution = kubernetesDistribution;\n }\n if (isMultiNode !== undefined) {\n body.is_multi_node = isMultiNode;\n }\n if (serviceAccountId !== undefined) {\n body.service_account_id = serviceAccountId;\n }\n if (adminConsoleUrl !== undefined) {\n body.admin_console_url = adminConsoleUrl;\n }\n if (status !== undefined) {\n body.status = status;\n }\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] updating install options via %s\", url.toString());\n }\n\n const response = await authenticatedFetch(url.toString(), {\n method: \"PATCH\",\n token,\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify(body),\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Update install options failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n return await response.json();\n}\n\n// =============================================================================\n// Helper - Filter Releases with Embedded Cluster Support\n// =============================================================================\n\n/**\n * Filters channel releases to only include those with embedded cluster installers.\n * Use this when populating the version dropdown for Linux installs.\n */\nexport function filterEmbeddedClusterReleases(releases: ChannelRelease[]): ChannelRelease[] {\n return releases.filter(\n (release) => release.installationTypes?.embeddedCluster?.version\n );\n}\n\n/**\n * Filters channel releases to only include those with Helm chart support.\n * Use this when populating the version dropdown for Helm installs.\n */\nexport function filterHelmReleases(releases: ChannelRelease[]): ChannelRelease[] {\n return releases.filter(\n (release) => release.installationTypes?.helm?.version\n );\n}\n\n// =============================================================================\n// Types - Update Instructions\n// =============================================================================\n\nexport interface GetUpdateInstructionsInput {\n token: string;\n /** Install options ID (if known) */\n installOptionsId?: string;\n /** Instance ID (used if installOptionsId is not available) */\n instanceId?: string;\n /** Target channel ID for the update */\n targetChannelId: string;\n /** Target channel sequence for the update */\n targetChannelSequence: number;\n /** Private registry hostname (for airgap with private registry) */\n privateRegistryHostname?: string;\n}\n\nexport interface UpdateInstructions {\n format: string;\n install_type: string;\n title: string;\n steps: InstallStep[];\n}\n\nexport interface GetUpdateInstructionsResult {\n instructions: UpdateInstructions;\n}\n\n// =============================================================================\n// Actions - Update Instructions\n// =============================================================================\n\n/**\n * Fetches update instructions for an instance.\n * Returns step-by-step commands for updating to a target version.\n */\nexport async function getUpdateInstructions(\n input: GetUpdateInstructionsInput,\n context?: PortalActionContext\n): Promise<GetUpdateInstructionsResult> {\n const {\n token,\n installOptionsId,\n instanceId,\n targetChannelId,\n targetChannelSequence,\n privateRegistryHostname\n } = input;\n\n if (!token || typeof token !== \"string\") {\n throw new Error(\"getUpdateInstructions requires a session token\");\n }\n\n if (!targetChannelId?.trim()) {\n throw new Error(\"Target channel ID is required\");\n }\n\n if (targetChannelSequence === undefined || targetChannelSequence === null) {\n throw new Error(\"Target channel sequence is required\");\n }\n\n // Need either installOptionsId or instanceId\n const identifier = installOptionsId || instanceId;\n if (!identifier) {\n throw new Error(\"Either installOptionsId or instanceId is required\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const origin = getApiOrigin();\n\n // Build query params\n const queryParams = new URLSearchParams();\n queryParams.set(\"targetChannelId\", targetChannelId.trim());\n queryParams.set(\"targetChannelSequence\", targetChannelSequence.toString());\n \n if (privateRegistryHostname?.trim()) {\n queryParams.set(\"privateRegistryHostname\", privateRegistryHostname.trim());\n }\n \n // When using instance_id, we still need a path parameter for the route\n // but it will be ignored by the backend in favor of the instance_id query param\n if (!installOptionsId && instanceId) {\n queryParams.set(\"instance_id\", instanceId);\n }\n\n const pathId = installOptionsId || \"placeholder\";\n const url = `${origin}/v3/customers/${customerId}/install-options/${pathId}/update-instructions?${queryParams.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching update instructions via %s\", url);\n }\n\n const response = await authenticatedFetch(url, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n let errorMessage = \"Failed to fetch update instructions\";\n try {\n const errorData = JSON.parse(errorText);\n errorMessage = errorData.error || errorData.message || errorMessage;\n } catch {\n // Use default message\n }\n throw new Error(errorMessage);\n }\n\n const data = await response.json();\n\n return {\n instructions: {\n format: data.format || \"steps\",\n install_type: data.install_type,\n title: data.title,\n steps: data.steps || []\n }\n };\n}\n\n// =============================================================================\n// Types - Pending Installations\n// =============================================================================\n\nexport interface FetchPendingInstallationsInput {\n token: string;\n}\n\nexport interface PendingInstallation {\n id: string;\n name: string;\n method: \"helm\" | \"linux\";\n startedBy: string;\n startedAt: string;\n}\n\nexport interface FetchPendingInstallationsResult {\n installations: PendingInstallation[];\n}\n\n// =============================================================================\n// Fetch Pending Installations\n// =============================================================================\n\n/**\n * Fetches pending (in-progress) installations for the current user.\n * Returns up to 5 most recent installations, ordered by creation date descending.\n */\nexport async function fetchPendingInstallations(\n input: FetchPendingInstallationsInput,\n context?: PortalActionContext\n): Promise<FetchPendingInstallationsResult> {\n const { token } = input;\n \n if (typeof token !== \"string\" || token.trim().length === 0) {\n throw new Error(\"fetchPendingInstallations requires a non-empty token\");\n }\n\n // Extract customer ID from token\n let customerId: string;\n try {\n customerId = getCustomerIdFromToken(token);\n } catch {\n return { installations: [] };\n }\n\n const origin = getApiOrigin();\n \n // Use the same endpoint and filtering as vandoor:\n // GET /v3/customers/{customerId}/install-options?status=in_progress&page_size=5\n const queryParams = new URLSearchParams();\n queryParams.set(\"status\", \"in_progress\");\n queryParams.set(\"page_size\", \"5\");\n queryParams.set(\"order_by\", \"created_at\");\n queryParams.set(\"order_direction\", \"desc\");\n \n const endpoint = `${origin}/v3/customers/${customerId}/install-options?${queryParams.toString()}`;\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Pending installations request failed (${response.status} ${response.statusText})`\n );\n }\n\n const payload = await response.json();\n \n // Log the raw response for debugging\n console.log(\"[portal-components] fetchPendingInstallations raw response:\", JSON.stringify(payload, null, 2));\n \n // Parse the response - expecting {install_options: [...]}\n const installArray = payload?.install_options || [];\n \n const installations: PendingInstallation[] = installArray.map((item: Record<string, unknown>) => ({\n id: String(item.id || \"\"),\n name: String(item.instance_name || \"Unknown\"),\n method: item.install_type === \"helm\" ? \"helm\" as const : \"linux\" as const,\n startedBy: String(item.service_account_email_address || \"Unknown\"),\n startedAt: String(item.started_at || new Date().toISOString())\n }));\n\n console.log(\"[portal-components] fetchPendingInstallations parsed installations:\", installations.length);\n\n return {\n installations\n };\n}\n\n"]}
1
+ {"version":3,"sources":["../../src/components/linux-install-wizard.tsx","../../src/utils/api-client.ts","../../src/actions/index.ts","../../src/actions/install.ts"],"names":[],"mappings":"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","file":"linux-install-wizard.js","sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { CreateServiceAccountResult } from \"../actions\";\nimport type {\n ChannelRelease,\n InstallInstructions,\n InstallStep,\n NetworkAvailability,\n CreateInstallOptionsResult,\n GetInstallOptionsResult,\n UpdateInstallOptionsResult,\n FetchChannelReleasesResult\n} from \"../actions/install\";\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nexport const LINUX_INSTALL_SERVICE_ACCOUNT_KEY = \"linux_install_service_account\";\nexport const LINUX_INSTALL_OPTIONS_KEY = \"linux_install_options\";\n\nexport interface LinuxInstallWizardProps {\n /** JWT token for authentication */\n token: string;\n /** Server action to create service account - REQUIRED */\n createServiceAccountAction: (name: string, token: string) => Promise<CreateServiceAccountResult>;\n /** Server action to fetch channel releases - REQUIRED for version dropdown */\n fetchChannelReleasesAction?: (token: string) => Promise<FetchChannelReleasesResult>;\n /** Server action to create install options - REQUIRED for tracking */\n createInstallOptionsAction?: (input: {\n token: string;\n installType: \"linux\";\n instanceName: string;\n serviceAccountId: string;\n networkAvailability: NetworkAvailability;\n isMultiNode?: boolean;\n channelId?: string;\n channelReleaseSequence?: number;\n }) => Promise<CreateInstallOptionsResult>;\n /** Server action to get install options - for resuming */\n getInstallOptionsAction?: (input: {\n token: string;\n installOptionsId: string;\n includeInstructions?: boolean;\n proxyUrl?: string;\n }) => Promise<GetInstallOptionsResult>;\n /** Server action to update install options - for version selection */\n updateInstallOptionsAction?: (input: {\n token: string;\n installOptionsId: string;\n channelId?: string;\n channelReleaseSequence?: number;\n adminConsoleUrl?: string | null;\n includeInstructions?: boolean;\n proxyUrl?: string;\n }) => Promise<UpdateInstallOptionsResult>;\n /** Callback when step changes */\n onStepChange?: (step: 1 | 2) => void;\n /** Callback when network availability changes */\n onNetworkChange?: (network: NetworkAvailability) => void;\n /** Callback when installOptionsId changes (for URL tracking) */\n onInstallOptionsIdChange?: (id: string | null) => void;\n /** Initial step to show (default: 1) */\n initialStep?: 1 | 2;\n /** Initial network availability (default: \"online\") */\n initialNetwork?: NetworkAvailability;\n /** Initial install options ID (for resuming) */\n initialInstallOptionsId?: string;\n /** Pre-fetched install options data (for SSR - skips client fetch) */\n initialInstallOptionsData?: GetInstallOptionsResult;\n /** Pre-fetched channel releases (for SSR - skips client fetch) */\n initialChannelReleases?: ChannelRelease[];\n}\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\nconst navigateTo = (href: string) => {\n try {\n if (typeof window === \"undefined\") {\n return;\n }\n window.location.assign(href);\n } catch (error) {\n console.error(\"[linux-install-wizard] navigation failed\", error);\n }\n};\n\n\nconst copyToClipboard = async (text: string): Promise<boolean> => {\n try {\n await navigator.clipboard.writeText(text);\n return true;\n } catch {\n return false;\n }\n};\n\n// =============================================================================\n// Sub-Components\n// =============================================================================\n\nconst StepIndicator = ({ step }: { step: 1 | 2 }) => (\n <div className=\"flex items-center justify-center gap-3\">\n <div\n className={`flex h-10 w-10 items-center justify-center rounded-full border-2 ${\n step > 1 ? \"border-gray-900 bg-gray-900 text-white\" : \"border-indigo-500\"\n }`}\n >\n {step > 1 ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 16 16\"\n className=\"h-3.5 w-3.5\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n >\n <path d=\"m3.5 8 3 3 6-6\" />\n </svg>\n ) : (\n <span className=\"h-2.5 w-2.5 rounded-full bg-indigo-500\" />\n )}\n </div>\n <div className={`h-0.5 w-12 ${step > 1 ? \"bg-gray-900\" : \"bg-gray-200\"}`} />\n <div\n className={`flex h-10 w-10 items-center justify-center rounded-full border-2 ${\n step === 2 ? \"border-gray-900\" : \"border-gray-200\"\n }`}\n >\n {step === 2 ? <span className=\"h-2.5 w-2.5 rounded-full bg-gray-900\" /> : null}\n </div>\n </div>\n);\n\nconst CodeBlock = ({ \n command, \n onCopy \n}: { \n command: string;\n onCopy?: () => void;\n}) => {\n const [copied, setCopied] = useState(false);\n\n const handleCopy = async () => {\n const success = await copyToClipboard(command);\n if (success) {\n setCopied(true);\n onCopy?.();\n setTimeout(() => setCopied(false), 2000);\n }\n };\n\n return (\n <div className=\"group relative ml-8 mt-2 min-w-0\">\n <pre className=\"overflow-x-auto whitespace-pre rounded-lg bg-gray-900 p-4 text-sm text-gray-100\">\n <code className=\"block\">{command}</code>\n </pre>\n <button\n type=\"button\"\n onClick={handleCopy}\n className=\"absolute right-2 top-2 rounded bg-gray-700 p-1.5 text-gray-300 opacity-0 transition hover:bg-gray-600 hover:text-white group-hover:opacity-100\"\n aria-label=\"Copy to clipboard\"\n >\n {copied ? (\n <svg className=\"h-4 w-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ) : (\n <svg className=\"h-4 w-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\" />\n </svg>\n )}\n </button>\n </div>\n );\n};\n\nconst VersionDropdown = ({\n releases,\n selectedRelease,\n onSelect,\n isLoading,\n error\n}: {\n releases: ChannelRelease[];\n selectedRelease: ChannelRelease | null;\n onSelect: (release: ChannelRelease) => void;\n isLoading: boolean;\n error: string | null;\n}) => {\n if (isLoading) {\n return (\n <div className=\"ml-8 flex items-center gap-2 text-sm text-gray-500\">\n <div className=\"h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-indigo-500\" />\n Loading versions...\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"ml-8 text-sm text-rose-600\">\n Failed to load versions: {error}\n </div>\n );\n }\n\n if (releases.length === 0) {\n return (\n <p className=\"ml-8 text-sm text-gray-500\">\n There is no Embedded Cluster installer in this release.\n </p>\n );\n }\n\n return (\n <div className=\"ml-8 space-y-2\">\n <label className=\"block text-sm text-gray-600\">App Version</label>\n <select\n value={selectedRelease?.channelSequence ?? \"\"}\n onChange={(e) => {\n const sequence = parseInt(e.target.value, 10);\n const release = releases.find(r => r.channelSequence === sequence);\n if (release) {\n onSelect(release);\n }\n }}\n className=\"portal-select w-full\"\n >\n {releases.map((release) => (\n <option key={release.channelSequence} value={release.channelSequence}>\n {release.versionLabel || `Sequence ${release.channelSequence}`}\n {release.channelName ? ` (${release.channelName})` : \"\"}\n </option>\n ))}\n </select>\n </div>\n );\n};\n\nconst CheckIcon = () => (\n <svg className=\"h-4 w-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2.5}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n);\n\nconst InstallationInstructions = ({\n instructions,\n isLoading,\n completedSteps = {}\n}: {\n instructions: InstallInstructions | null;\n isLoading: boolean;\n completedSteps?: Record<string, boolean>;\n}) => {\n // Only show loading spinner if we don't have any instructions yet\n if (isLoading && !instructions?.steps?.length) {\n return (\n <div className=\"space-y-4\">\n <h3 className=\"text-lg font-semibold text-gray-900\">Installation Instructions</h3>\n <div className=\"flex items-center gap-2 text-sm text-gray-500\">\n <div className=\"h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-indigo-500\" />\n Loading instructions...\n </div>\n </div>\n );\n }\n\n if (!instructions?.steps?.length) {\n return null;\n }\n\n return (\n <div className={`min-w-0 space-y-4 transition-opacity duration-150 ${isLoading ? \"opacity-60\" : \"\"}`}>\n <h3 className=\"text-lg font-semibold text-gray-900\">\n Installation Instructions\n {isLoading && (\n <span className=\"ml-2 inline-block h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-indigo-500\" />\n )}\n </h3>\n <ol className=\"min-w-0 space-y-6 text-sm text-gray-700\">\n {instructions.steps.map((step: InstallStep, index: number) => {\n // Steps that can show completion checkmarks (based on step_name from backend)\n // These are the steps that track progress: download_assets and install\n const completableStepNames = [\"download_assets\", \"install\"];\n const canComplete = completableStepNames.includes(step.step_name);\n const isCompleted = canComplete && completedSteps[step.step_name];\n return (\n <li key={step.step_number} className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n {/* Always show step number */}\n <span className=\"flex h-6 w-6 items-center justify-center rounded-full bg-indigo-100 font-semibold text-indigo-500\">\n {index + 2}\n </span>\n <span className=\"font-medium text-gray-900\">{step.title}</span>\n {/* Show checkmark to the right of title for completable steps */}\n {canComplete && (\n <span \n className={`flex h-5 w-5 items-center justify-center rounded-full ${\n isCompleted \n ? \"bg-green-500 text-white\" \n : \"bg-gray-200 text-gray-400\"\n }`}\n >\n <CheckIcon />\n </span>\n )}\n {isCompleted && (\n <span className=\"text-xs text-green-600\">Completed</span>\n )}\n </div>\n {step.description && (\n <p className=\"ml-8 text-gray-500\">{step.description}</p>\n )}\n {step.commands.map((command, cmdIndex) => (\n <CodeBlock key={cmdIndex} command={command} />\n ))}\n </li>\n );\n })}\n </ol>\n </div>\n );\n};\n\n// =============================================================================\n// Main Component\n// =============================================================================\n\nexport const LinuxInstallWizard = ({\n token,\n createServiceAccountAction,\n fetchChannelReleasesAction,\n createInstallOptionsAction,\n getInstallOptionsAction,\n updateInstallOptionsAction,\n onStepChange,\n onNetworkChange,\n onInstallOptionsIdChange,\n initialStep,\n initialNetwork,\n initialInstallOptionsId,\n initialInstallOptionsData,\n initialChannelReleases\n}: LinuxInstallWizardProps) => {\n // Basic wizard state - initialize from pre-fetched data if available\n const [step, setStep] = useState<1 | 2>(initialStep ?? 1);\n const [instanceName, setInstanceName] = useState(initialInstallOptionsData?.instance_name ?? \"\");\n const [networkAvailability, setNetworkAvailability] = useState<NetworkAvailability>(initialNetwork ?? \"online\");\n const [adminConsoleUrl, setAdminConsoleUrl] = useState(initialInstallOptionsData?.admin_console_url ?? \"https://localhost:30000\");\n const [proxyUrl, setProxyUrl] = useState(\"\");\n const [showErrors, setShowErrors] = useState(false);\n const [isCreatingServiceAccount, setIsCreatingServiceAccount] = useState(false);\n const [apiError, setApiError] = useState<string | null>(null);\n\n // Install options state - initialize from pre-fetched data if available\n const [installOptionsId, setInstallOptionsId] = useState<string | null>(initialInstallOptionsId ?? null);\n const [serviceAccountId, setServiceAccountId] = useState<string | null>(initialInstallOptionsData?.service_account_id ?? null);\n // Track the original instance name used when service account was created (for back button handling)\n const [originalInstanceName, setOriginalInstanceName] = useState<string | null>(initialInstallOptionsData?.instance_name ?? null);\n\n // Channel releases state - initialize from pre-fetched data if available\n const [releases, setReleases] = useState<ChannelRelease[]>(initialChannelReleases ?? []);\n const [selectedRelease, setSelectedRelease] = useState<ChannelRelease | null>(() => {\n // If we have pre-fetched data, find the matching release\n if (initialInstallOptionsData?.channel_id && initialInstallOptionsData?.channel_release_sequence && initialChannelReleases) {\n return initialChannelReleases.find(\n r => r.channelId === initialInstallOptionsData.channel_id && \n r.channelSequence === initialInstallOptionsData.channel_release_sequence\n ) ?? null;\n }\n return null;\n });\n const [isLoadingReleases, setIsLoadingReleases] = useState(false);\n const [releasesError, setReleasesError] = useState<string | null>(null);\n\n // Instructions state - initialize from pre-fetched data if available\n const [instructions, setInstructions] = useState<InstallInstructions | null>(initialInstallOptionsData?.instructions ?? null);\n const [isLoadingInstructions, setIsLoadingInstructions] = useState(false);\n const [completedSteps, setCompletedSteps] = useState<Record<string, boolean>>({});\n\n // Refs for tracking state - mark as loaded if we have pre-fetched data\n const hasLoadedReleases = useRef(!!initialChannelReleases?.length);\n const hasResumedInstallation = useRef(!!initialInstallOptionsData);\n const lastUpdateRef = useRef<{ channelId?: string; sequence?: number }>(\n initialInstallOptionsData?.channel_id && initialInstallOptionsData?.channel_release_sequence\n ? { channelId: initialInstallOptionsData.channel_id, sequence: initialInstallOptionsData.channel_release_sequence }\n : {}\n );\n\n // Filter releases to only those with Embedded Cluster support\n const embeddedClusterReleases = useMemo(() => {\n return releases.filter(r => r.installationTypes?.embeddedCluster?.version);\n }, [releases]);\n\n // ==========================================================================\n // Effects - Sync with props\n // ==========================================================================\n\n // Only sync step from external prop when it changes (for URL-based navigation)\n // Using a ref to track the previous initialStep to avoid resetting on internal step changes\n const prevInitialStepRef = useRef(initialStep);\n useEffect(() => {\n if (initialStep !== undefined && initialStep !== prevInitialStepRef.current) {\n setStep(initialStep);\n }\n prevInitialStepRef.current = initialStep;\n }, [initialStep]);\n\n useEffect(() => {\n if (initialNetwork !== undefined && initialNetwork !== networkAvailability) {\n setNetworkAvailability(initialNetwork);\n }\n }, [initialNetwork, networkAvailability]);\n\n useEffect(() => {\n onStepChange?.(step);\n }, [step, onStepChange]);\n\n useEffect(() => {\n onNetworkChange?.(networkAvailability);\n }, [networkAvailability, onNetworkChange]);\n\n useEffect(() => {\n onInstallOptionsIdChange?.(installOptionsId);\n }, [installOptionsId, onInstallOptionsIdChange]);\n\n // ==========================================================================\n // Effects - Load releases on step 2\n // ==========================================================================\n\n useEffect(() => {\n if (step !== 2 || !fetchChannelReleasesAction || hasLoadedReleases.current) {\n return;\n }\n\n const loadReleases = async () => {\n setIsLoadingReleases(true);\n setReleasesError(null);\n\n try {\n const result = await fetchChannelReleasesAction(token);\n setReleases(result.channelReleases || []);\n hasLoadedReleases.current = true;\n\n // Auto-select first release with EC support\n const ecReleases = (result.channelReleases || []).filter(\n r => r.installationTypes?.embeddedCluster?.version\n );\n const firstRelease = ecReleases[0];\n if (firstRelease && !selectedRelease) {\n setSelectedRelease(firstRelease);\n }\n } catch (error) {\n console.error(\"[linux-install-wizard] Failed to load releases\", error);\n setReleasesError(error instanceof Error ? error.message : \"Failed to load releases\");\n } finally {\n setIsLoadingReleases(false);\n }\n };\n\n loadReleases();\n }, [step, token, fetchChannelReleasesAction, selectedRelease]);\n\n // Auto-select first release when pre-fetched releases are available (for new installs)\n const hasAutoSelectedRelease = useRef(false);\n useEffect(() => {\n if (hasAutoSelectedRelease.current || selectedRelease) {\n return;\n }\n const firstRelease = embeddedClusterReleases[0];\n if (step === 2 && firstRelease) {\n setSelectedRelease(firstRelease);\n hasAutoSelectedRelease.current = true;\n }\n }, [step, embeddedClusterReleases, selectedRelease]);\n\n // ==========================================================================\n // Effects - Update install options when release changes\n // ==========================================================================\n\n useEffect(() => {\n if (!selectedRelease || !installOptionsId || !updateInstallOptionsAction) {\n return;\n }\n\n const channelId = selectedRelease.channelId;\n const sequence = selectedRelease.channelSequence;\n\n // Skip if we already updated for this release\n if (\n lastUpdateRef.current.channelId === channelId &&\n lastUpdateRef.current.sequence === sequence\n ) {\n return;\n }\n\n const updateOptions = async () => {\n setIsLoadingInstructions(true);\n\n try {\n const result = await updateInstallOptionsAction({\n token,\n installOptionsId,\n channelId,\n channelReleaseSequence: sequence,\n includeInstructions: true,\n proxyUrl: networkAvailability === \"proxy\" ? proxyUrl : undefined\n });\n\n lastUpdateRef.current = { channelId, sequence };\n\n if (result.instructions) {\n setInstructions(result.instructions);\n }\n } catch (error) {\n console.error(\"[linux-install-wizard] Failed to update install options\", error);\n setApiError(error instanceof Error ? error.message : \"Failed to update install options\");\n } finally {\n setIsLoadingInstructions(false);\n }\n };\n\n updateOptions();\n }, [selectedRelease, installOptionsId, token, updateInstallOptionsAction, networkAvailability, proxyUrl]);\n\n // ==========================================================================\n // Effects - Resume from install options ID\n // ==========================================================================\n\n useEffect(() => {\n // Skip if we already have pre-fetched data or already resumed\n if (hasResumedInstallation.current) {\n return;\n }\n if (!initialInstallOptionsId || !getInstallOptionsAction || step !== 2) {\n return;\n }\n\n hasResumedInstallation.current = true;\n\n const resumeInstallation = async () => {\n setIsLoadingInstructions(true);\n\n try {\n const result = await getInstallOptionsAction({\n token,\n installOptionsId: initialInstallOptionsId,\n includeInstructions: true,\n proxyUrl: networkAvailability === \"proxy\" ? proxyUrl : undefined\n });\n\n // Restore state from install options\n if (result.instance_name) {\n setInstanceName(result.instance_name);\n }\n if (result.service_account_id) {\n setServiceAccountId(result.service_account_id);\n }\n if (result.instructions) {\n setInstructions(result.instructions);\n }\n if (result.admin_console_url) {\n setAdminConsoleUrl(result.admin_console_url);\n }\n\n // Find and set the selected release\n if (result.channel_id && result.channel_release_sequence) {\n const matchingRelease = releases.find(\n r => r.channelId === result.channel_id && \n r.channelSequence === result.channel_release_sequence\n );\n if (matchingRelease) {\n setSelectedRelease(matchingRelease);\n lastUpdateRef.current = {\n channelId: result.channel_id,\n sequence: result.channel_release_sequence\n };\n }\n }\n } catch (error) {\n console.error(\"[linux-install-wizard] Failed to resume installation\", error);\n setApiError(error instanceof Error ? error.message : \"Failed to resume installation\");\n } finally {\n setIsLoadingInstructions(false);\n }\n };\n\n resumeInstallation();\n }, [initialInstallOptionsId, getInstallOptionsAction, token, step, releases, networkAvailability, proxyUrl]);\n\n // ==========================================================================\n // Effects - Poll for step completion status\n // ==========================================================================\n\n useEffect(() => {\n // Only poll when on Step 2 with an installOptionsId and getInstallOptionsAction available\n if (step !== 2 || !installOptionsId || !getInstallOptionsAction) {\n return;\n }\n\n // Check if both steps are already completed - no need to poll\n if (completedSteps[\"download_assets\"] && completedSteps[\"install\"]) {\n return;\n }\n\n const pollInterval = setInterval(async () => {\n try {\n const result = await getInstallOptionsAction({\n token,\n installOptionsId,\n includeInstructions: false, // We don't need instructions for status polling\n });\n\n // Update completed steps based on timestamps\n const newCompletedSteps: Record<string, boolean> = {};\n \n if (result.assets_downloaded_at) {\n newCompletedSteps[\"download_assets\"] = true;\n }\n if (result.installation_completed_at) {\n newCompletedSteps[\"install\"] = true;\n }\n\n // Only update state if something changed\n if (\n newCompletedSteps[\"download_assets\"] !== completedSteps[\"download_assets\"] ||\n newCompletedSteps[\"install\"] !== completedSteps[\"install\"]\n ) {\n setCompletedSteps(prev => ({ ...prev, ...newCompletedSteps }));\n }\n\n // Stop polling if both are complete\n if (newCompletedSteps[\"download_assets\"] && newCompletedSteps[\"install\"]) {\n clearInterval(pollInterval);\n }\n } catch {\n // Silently ignore polling errors\n }\n }, 2000);\n\n return () => clearInterval(pollInterval);\n }, [step, installOptionsId, getInstallOptionsAction, token, completedSteps]);\n\n // ==========================================================================\n // Handlers\n // ==========================================================================\n\n const handleContinue = async () => {\n console.debug(\"[linux-install-wizard] handleContinue called, instanceName:\", JSON.stringify(instanceName));\n if (!instanceName.trim()) {\n console.debug(\"[linux-install-wizard] Validation failed: instanceName is empty\");\n setShowErrors(true);\n return;\n }\n\n if (!token) {\n setApiError(\"Configuration error: authentication token is required\");\n console.error(\"[linux-install-wizard] token prop is required but was not provided\");\n return;\n }\n\n setShowErrors(false);\n setApiError(null);\n setIsCreatingServiceAccount(true);\n\n try {\n const trimmedInstanceName = instanceName.trim();\n \n // Branch 3: Service account exists AND instance name is the same (user went back but kept same name)\n // Just update install options, don't create new service account\n if (serviceAccountId && originalInstanceName === trimmedInstanceName && installOptionsId && updateInstallOptionsAction) {\n console.debug(\"[linux-install-wizard] Reusing existing service account, updating install options...\");\n \n const firstRelease = embeddedClusterReleases[0];\n \n const result = await updateInstallOptionsAction({\n token,\n installOptionsId,\n channelId: firstRelease?.channelId,\n channelReleaseSequence: firstRelease?.channelSequence,\n includeInstructions: true\n });\n \n if (firstRelease) {\n setSelectedRelease(firstRelease);\n lastUpdateRef.current = {\n channelId: firstRelease.channelId,\n sequence: firstRelease.channelSequence\n };\n }\n \n if (result.instructions) {\n setInstructions(result.instructions);\n }\n \n console.debug(\"[linux-install-wizard] Transitioning to step 2 (reused service account)\");\n setStep(2);\n return;\n }\n \n // Branch 1 & 2: Need to create new service account\n // (Either no service account exists, or instance name changed)\n \n // Delete existing data from session storage\n if (typeof window !== \"undefined\" && window.sessionStorage) {\n window.sessionStorage.removeItem(LINUX_INSTALL_SERVICE_ACCOUNT_KEY);\n window.sessionStorage.removeItem(LINUX_INSTALL_OPTIONS_KEY);\n }\n\n // Create service account\n const saData = await createServiceAccountAction(trimmedInstanceName, token);\n\n // Store in session storage\n if (typeof window !== \"undefined\" && window.sessionStorage) {\n window.sessionStorage.setItem(LINUX_INSTALL_SERVICE_ACCOUNT_KEY, JSON.stringify(saData));\n }\n\n setServiceAccountId(saData.service_account.id);\n setOriginalInstanceName(trimmedInstanceName);\n\n // Create install options (if action provided)\n if (createInstallOptionsAction) {\n console.debug(\"[linux-install-wizard] Creating install options...\");\n \n // Get the first available release to include with install options\n // This allows instructions to be generated immediately\n const firstRelease = embeddedClusterReleases[0];\n \n const installOptionsResult = await createInstallOptionsAction({\n token,\n installType: \"linux\",\n instanceName: trimmedInstanceName,\n serviceAccountId: saData.service_account.id,\n networkAvailability: networkAvailability,\n isMultiNode: false,\n channelId: firstRelease?.channelId,\n channelReleaseSequence: firstRelease?.channelSequence\n });\n console.debug(\"[linux-install-wizard] Install options result:\", installOptionsResult);\n \n // Set the selected release to match what we sent\n if (firstRelease) {\n setSelectedRelease(firstRelease);\n lastUpdateRef.current = {\n channelId: firstRelease.channelId,\n sequence: firstRelease.channelSequence\n };\n }\n\n // Handle both response formats: { install_options: { id: ... } } and { id: ... }\n const optionsId = installOptionsResult.install_options?.id ?? (installOptionsResult as any).id;\n if (!optionsId) {\n throw new Error(\"Install options response did not contain an ID\");\n }\n setInstallOptionsId(optionsId);\n\n // Store in session storage\n if (typeof window !== \"undefined\" && window.sessionStorage) {\n window.sessionStorage.setItem(LINUX_INSTALL_OPTIONS_KEY, JSON.stringify(installOptionsResult));\n }\n\n // If we got instructions back, store them\n if (installOptionsResult.instructions) {\n setInstructions(installOptionsResult.instructions);\n }\n\n // Notify parent of new install options ID\n onInstallOptionsIdChange?.(optionsId);\n }\n\n console.debug(\"[linux-install-wizard] Transitioning to step 2\");\n setStep(2);\n } catch (error) {\n console.error(\"[linux-install-wizard] Failed to continue\", error);\n const errorMessage = error instanceof Error ? error.message : \"Failed to continue\";\n // Provide a friendlier error message for \"already exists\" errors\n if (errorMessage.toLowerCase().includes(\"already exists\")) {\n setApiError(\"Instance name must be unique. Please choose a different name.\");\n } else {\n setApiError(errorMessage);\n }\n } finally {\n setIsCreatingServiceAccount(false);\n }\n };\n\n const handleBack = () => {\n // Clear errors when going back\n setApiError(null);\n setShowErrors(false);\n setStep(1);\n };\n\n const handleFinish = useCallback(async () => {\n // Update admin console URL if changed\n if (installOptionsId && updateInstallOptionsAction && adminConsoleUrl) {\n try {\n await updateInstallOptionsAction({\n token,\n installOptionsId,\n adminConsoleUrl\n });\n } catch (error) {\n console.error(\"[linux-install-wizard] Failed to save admin console URL\", error);\n // Don't block navigation on this error\n }\n }\n\n navigateTo(\"/update\");\n }, [installOptionsId, updateInstallOptionsAction, adminConsoleUrl, token]);\n\n const handleReleaseSelect = useCallback((release: ChannelRelease) => {\n setSelectedRelease(release);\n }, []);\n\n const isProxy = networkAvailability === \"proxy\";\n\n // ==========================================================================\n // Render Step 2\n // ==========================================================================\n\n if (step === 2) {\n return (\n <div className=\"space-y-6\">\n <StepIndicator step={2} />\n <div className=\"overflow-hidden rounded-2xl border border-gray-100 bg-gray-50 p-6\">\n <div className=\"min-w-0 space-y-6\">\n <div className=\"min-w-0\">\n <h2 className=\"text-xl font-semibold text-gray-900\">\n {isProxy ? \"Linux Single Node Proxy Install\" : \"Linux Single Node Online Install\"}\n </h2>\n\n <div className=\"mt-6 space-y-6\">\n {/* Step 1: Version Selection */}\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-indigo-500\">\n <span className=\"flex h-6 w-6 items-center justify-center rounded-full bg-indigo-100 font-semibold\">1</span>\n <span className=\"font-medium text-gray-900\">Select a version</span>\n </div>\n <VersionDropdown\n releases={embeddedClusterReleases}\n selectedRelease={selectedRelease}\n onSelect={handleReleaseSelect}\n isLoading={isLoadingReleases}\n error={releasesError}\n />\n </div>\n\n {/* Step 2 (Proxy only): Configure proxy URL */}\n {isProxy && (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-indigo-500\">\n <span className=\"flex h-6 w-6 items-center justify-center rounded-full bg-indigo-100 font-semibold\">2</span>\n <span className=\"font-medium text-gray-900\">Configure proxy URL</span>\n </div>\n <input\n value={proxyUrl}\n onChange={(event) => setProxyUrl(event.target.value)}\n placeholder=\"Enter proxy URL\"\n className=\"portal-input ml-8 w-full\"\n />\n </div>\n )}\n\n {/* Installation Instructions */}\n {selectedRelease && (\n <InstallationInstructions\n instructions={instructions}\n isLoading={isLoadingInstructions}\n completedSteps={completedSteps}\n />\n )}\n\n {/* Admin Console URL (optional) */}\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-indigo-500\">\n <span className=\"flex h-6 w-6 items-center justify-center rounded-full bg-indigo-100 font-semibold\">\n {(instructions?.steps?.length ?? 0) + (isProxy ? 3 : 2)}\n </span>\n <span className=\"font-medium text-gray-900\">(Optional) Add the Admin Console URL</span>\n </div>\n <input\n value={adminConsoleUrl}\n onChange={(event) => setAdminConsoleUrl(event.target.value)}\n placeholder=\"https://admin-console.example.com\"\n className=\"portal-input ml-8 w-full\"\n />\n </div>\n </div>\n </div>\n\n {apiError && (\n <div className=\"rounded-xl border border-rose-200 bg-rose-50 p-3 text-sm text-rose-600\">\n {apiError}\n </div>\n )}\n\n <div className=\"flex items-center justify-between text-sm text-gray-500\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"rounded-xl px-4 py-2 font-medium text-gray-500 transition hover:bg-gray-100\"\n >\n Back\n </button>\n <span>Step 2 of 2</span>\n <button\n type=\"button\"\n onClick={handleFinish}\n className=\"rounded-xl bg-indigo-500 px-4 py-2 font-medium text-white transition hover:bg-indigo-600\"\n >\n Finish\n </button>\n </div>\n\n <p className=\"text-center text-xs text-gray-400\">\n Having trouble? <a className=\"text-indigo-500\" href=\"#\">Open an issue.</a>\n </p>\n </div>\n </div>\n </div>\n );\n }\n\n // ==========================================================================\n // Render Step 1\n // ==========================================================================\n\n return (\n <div className=\"space-y-6\">\n <StepIndicator step={1} />\n\n <div className=\"rounded-2xl border border-gray-100 bg-gray-50 p-6\">\n <div className=\"space-y-4\">\n <label className=\"block text-sm font-medium text-gray-700\">\n Instance Name\n <input\n value={instanceName}\n onChange={(event) => setInstanceName(event.target.value)}\n placeholder=\"Instance nickname\"\n aria-invalid={showErrors && !instanceName.trim()}\n className={`portal-input mt-1 w-full ${\n showErrors && !instanceName.trim()\n ? \"border-rose-400 focus:border-rose-400 focus:ring-rose-200\"\n : \"\"\n }`}\n />\n {showErrors && !instanceName.trim() ? (\n <span className=\"mt-1 block text-xs text-rose-500\">Instance name is required.</span>\n ) : null}\n </label>\n\n <fieldset className=\"space-y-2\">\n <legend className=\"text-sm font-medium text-gray-700\">Network Availability</legend>\n <label className=\"flex items-center gap-3 text-sm text-gray-600\">\n <input\n type=\"radio\"\n name=\"network-availability\"\n checked={networkAvailability === \"online\"}\n onChange={() => setNetworkAvailability(\"online\")}\n className=\"portal-radio\"\n />\n Outbound requests allowed\n </label>\n <label className=\"flex items-center gap-3 text-sm text-gray-600\">\n <input\n type=\"radio\"\n name=\"network-availability\"\n checked={networkAvailability === \"proxy\"}\n onChange={() => setNetworkAvailability(\"proxy\")}\n className=\"portal-radio\"\n />\n Outbound requests require HTTPS Proxy\n </label>\n <label className=\"flex items-center gap-3 text-sm text-gray-600\">\n <input\n type=\"radio\"\n name=\"network-availability\"\n checked={networkAvailability === \"airgap\"}\n onChange={() => setNetworkAvailability(\"airgap\")}\n className=\"portal-radio\"\n />\n No outbound requests allowed (air gap)\n </label>\n </fieldset>\n\n {apiError ? (\n <div className=\"rounded-xl border border-rose-200 bg-rose-50 p-3 text-sm text-rose-600\">\n {apiError}\n </div>\n ) : null}\n </div>\n\n <div className=\"mt-6 flex items-center justify-between text-sm text-gray-500\">\n <span>Step 1 of 2</span>\n <button\n type=\"button\"\n onClick={handleContinue}\n disabled={isCreatingServiceAccount}\n className=\"rounded-xl bg-indigo-500 px-4 py-2 font-medium text-white transition hover:bg-indigo-600 disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {isCreatingServiceAccount ? \"Creating...\" : \"Continue\"}\n </button>\n </div>\n </div>\n </div>\n );\n};\n\nLinuxInstallWizard.displayName = \"LinuxInstallWizard\";\n","/**\n * Centralized API client utility for handling authenticated requests\n * with automatic 401 detection, cookie deletion, and redirect.\n */\n\nexport interface ApiFetchOptions extends RequestInit {\n token?: string;\n}\n\nexport class UnauthorizedError extends Error {\n constructor(message = \"Unauthorized\") {\n super(message);\n this.name = \"UnauthorizedError\";\n }\n}\n\n/**\n * Helper to check if an error is a Next.js redirect error.\n * These errors should NOT be caught as they control navigation flow.\n */\nexport function isRedirectError(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"digest\" in error &&\n typeof (error as { digest?: unknown }).digest === \"string\" &&\n (error as { digest: string }).digest.startsWith(\"NEXT_REDIRECT\")\n );\n}\n\n/**\n * Fetch wrapper that automatically handles error responses by:\n * - 401: Redirecting to \"/\" with expired parameter\n * - 502, 503, 504: Redirecting to \"/error\" with status code\n * \n * This function should be used for all authenticated API calls in server actions.\n * \n * IMPORTANT: This must be called from a Server Action context, not directly from\n * a Server Component, because it modifies cookies.\n */\nexport async function authenticatedFetch(\n url: string,\n options: ApiFetchOptions = {}\n): Promise<Response> {\n const { token, ...fetchOptions } = options;\n\n // Add authorization header if token is provided\n const headers = new Headers(fetchOptions.headers);\n if (token) {\n headers.set(\"authorization\", `Bearer ${token}`);\n }\n\n const response = await fetch(url, {\n ...fetchOptions,\n headers\n });\n\n // Handle 401 Unauthorized\n if (response.status === 401) {\n await handle401();\n }\n\n // Handle server errors (502, 503, 504) by redirecting to error page\n if (response.status === 502 || response.status === 503 || response.status === 504) {\n await handleServerError(response.status);\n }\n\n return response;\n}\n\n/**\n * Handles 401 unauthorized responses by redirecting to the root path with a logout parameter.\n * \n * Note: We cannot delete cookies here because this is called during Server Component\n * render, not from a true Server Action invocation.\n * \n * The home page MUST check for the `?expired=1` parameter and delete the portal_session\n * cookie when present to avoid infinite loops. Example:\n * \n * ```typescript\n * // In your home page (app/page.tsx)\n * import { cookies } from \"next/headers\";\n * \n * export default async function Home({ searchParams }: { searchParams: { expired?: string } }) {\n * if (searchParams.expired === \"1\") {\n * const cookieStore = await cookies();\n * cookieStore.delete(\"portal_session\");\n * }\n * \n * const sessionStore = await cookies();\n * const session = sessionStore.get(\"portal_session\");\n * // ... rest of your logic\n * }\n * ```\n */\nasync function handle401(): Promise<never> {\n const { redirect } = await import(\"next/navigation\");\n \n // Redirect with expired parameter so the home page can delete the cookie\n return redirect(\"/?expired=1\");\n}\n\n/**\n * Handles server errors (502, 503, 504) by redirecting to the error page.\n * \n * The error page route should be created at `app/error/page.tsx` in the consuming\n * application and use the ErrorPage component. Example:\n * \n * ```typescript\n * // In your error page (app/error/page.tsx)\n * import { ErrorPage } from \"@replicated/portal-components/error-page\";\n * \n * export default function Error({ \n * searchParams \n * }: { \n * searchParams: { code?: string; source?: string } \n * }) {\n * const statusCode = searchParams.code ? parseInt(searchParams.code, 10) : undefined;\n * const sourceUrl = searchParams.source;\n * return <ErrorPage statusCode={statusCode} sourceUrl={sourceUrl} />;\n * }\n * ```\n */\nasync function handleServerError(statusCode: number): Promise<never> {\n const { redirect } = await import(\"next/navigation\");\n \n // Try to get the current URL from Next.js headers\n let sourceUrl: string | undefined;\n try {\n const { headers } = await import(\"next/headers\");\n const headersList = await headers();\n const referer = headersList.get(\"referer\");\n const host = headersList.get(\"host\");\n const pathname = headersList.get(\"x-invoke-path\") || headersList.get(\"x-forwarded-path\");\n \n if (referer) {\n sourceUrl = referer;\n } else if (host && pathname) {\n const protocol = headersList.get(\"x-forwarded-proto\") || \"https\";\n sourceUrl = `${protocol}://${host}${pathname}`;\n }\n } catch (error) {\n // If we can't get headers, continue without source URL\n console.debug(\"[portal-components] Could not determine source URL\", error);\n }\n \n // Redirect to error page with status code and source URL parameters\n const params = new URLSearchParams({ code: String(statusCode) });\n if (sourceUrl) {\n params.set(\"source\", sourceUrl);\n }\n \n return redirect(`/error?${params.toString()}`);\n}\n","/**\n * Light-weight type helpers for defining Server Actions that align with the\n * enterprise portal guardrails. The component library does not implement\n * specific actions, but it exports helpers so downstream portals can describe\n * their actions with consistent metadata.\n */\n\nimport { Buffer } from \"node:buffer\";\nimport { cache } from \"react\";\nimport { authenticatedFetch } from \"../utils/api-client\";\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Gets the base API origin from environment, with trailing slashes removed.\n */\nexport const getApiOrigin = (): string => {\n return (process.env.REPLICATED_APP_ORIGIN || \"https://replicated.app\").replace(/\\/+$/, \"\");\n};\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport type PortalActionVisibility = \"vendor\" | \"customer\";\n\nexport interface PortalActionContext {\n vendorId: string;\n licenseId: string;\n userId: string;\n signal?: AbortSignal;\n}\n\nexport interface PortalServerActionDefinition<Input, Output> {\n id: string;\n description: string;\n visibility: PortalActionVisibility;\n tags: string[];\n run: (input: Input, context?: PortalActionContext) => Promise<Output>;\n}\n\nexport const defineServerAction = <Input, Output>(\n definition: PortalServerActionDefinition<Input, Output>\n) => definition;\n\nexport interface CreateServiceAccountInput {\n token: string;\n name: string;\n}\n\nexport interface ServiceAccountData {\n id: string;\n customerId: string;\n token: string;\n accountName: string;\n isRevoked: boolean;\n createdAt: string;\n emailAddress: string;\n}\n\nexport interface CreateServiceAccountResult {\n service_account: ServiceAccountData;\n token: string;\n}\n\nexport const createServiceAccount = defineServerAction<\n CreateServiceAccountInput,\n CreateServiceAccountResult\n>({\n id: \"service-account/create\",\n description: \"Creates a service account for installing applications\",\n visibility: \"customer\",\n tags: [\"service-account\", \"install\"],\n async run({ token, name }) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Service account creation requires a session token\");\n }\n\n if (!name || typeof name !== \"string\" || !name.trim()) {\n throw new Error(\"Service account name is required\");\n }\n\n // NEW: Use Enterprise Portal API endpoint\n const endpoint = `${getApiOrigin()}/enterprise-portal/team/service-accounts`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\n \"[portal-components] creating service account via %s (Enterprise Portal API)\",\n endpoint\n );\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"POST\",\n token,\n headers: {\n \"content-type\": \"application/json\"\n },\n body: JSON.stringify({ account_name: name.trim() })\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Service account creation failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n const data: CreateServiceAccountResult = await response.json();\n return data;\n }\n});\n\nexport interface InitiateLoginInput {\n email: string;\n}\n\nexport interface InitiateLoginResult {\n status: \"ok\" | \"saml_redirect\";\n requestedAt: string;\n message: string;\n /** If SAML redirect is required, this contains the info needed to redirect */\n saml?: {\n redirectRequired: true;\n customerId: string;\n email: string;\n appSlug: string;\n };\n}\n\n/**\n * Reference server action for initiating the passwordless login flow.\n * Real portals should replace the simulated delay with a call to their auth API.\n */\nexport const initiateLogin = defineServerAction<\n InitiateLoginInput,\n InitiateLoginResult\n>({\n id: \"auth/initiate-login\",\n description:\n \"Begins the passwordless login flow by dispatching a magic link email.\",\n visibility: \"customer\",\n tags: [\"auth\", \"login\", \"session\"],\n async run(input) {\n // NEW: Use Enterprise Portal API auth endpoint\n const endpoint = `${getApiOrigin()}/enterprise-portal/auth/magic-link`;\n const appSlug = process.env.PORTAL_APP_SLUG;\n if (!appSlug) {\n throw new Error(\"PORTAL_APP_SLUG is not configured\");\n }\n const portalOrigin =\n process.env.PORTAL_ORIGIN ?? \"https://enterprise.replicated.com\";\n const redirectUri = `${portalOrigin.replace(/\\/+$/, \"\")}/${appSlug}/login`;\n\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\"\n },\n body: JSON.stringify({\n app_slug: appSlug,\n email_address: input.email,\n redirect_uri: redirectUri\n })\n });\n\n if (!response.ok) {\n throw new Error(\n `Magic link request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n\n // Check if SAML redirect is required\n if (data.saml_redirect_required && data.saml_customer_id) {\n return {\n status: \"saml_redirect\",\n requestedAt: new Date().toISOString(),\n message: \"SAML authentication required\",\n saml: {\n redirectRequired: true,\n customerId: data.saml_customer_id,\n email: input.email,\n appSlug\n }\n };\n }\n\n return {\n status: \"ok\",\n requestedAt: new Date().toISOString(),\n message: `Magic link requested for ${input.email}`\n };\n }\n});\n\nexport interface VerifyMagicLinkInput {\n nonce: string;\n}\n\nexport interface VerifyMagicLinkResult {\n token: string;\n raw: unknown;\n}\n\nexport interface VerifyMagicLinkError {\n code: \"invalid_code\" | \"expired\" | \"unknown\";\n message: string;\n isExpired?: boolean;\n}\n\nexport const verifyMagicLink = defineServerAction<\n VerifyMagicLinkInput,\n VerifyMagicLinkResult\n>({\n id: \"auth/verify-magic-link\",\n description: \"Verifies the 12-digit code provided via email and returns a JWT.\",\n visibility: \"customer\",\n tags: [\"auth\", \"login\", \"verify\"],\n async run({ nonce }) {\n // NEW: Use Enterprise Portal API auth endpoint\n const endpoint = `${getApiOrigin()}/enterprise-portal/auth/magic-link/verify`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\n \"[portal-components] verifying magic link via %s\",\n endpoint\n );\n }\n\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\"\n },\n body: JSON.stringify({ nonce })\n });\n\n if (!response.ok) {\n if (response.status === 401) {\n // Check if the response indicates an expired link\n try {\n const errorBody = await response.json();\n if (errorBody?.is_expired === true) {\n const error: VerifyMagicLinkError = {\n code: \"expired\",\n message: \"Magic link has expired. A new link has been sent to your email.\",\n isExpired: true\n };\n throw error;\n }\n } catch (parseError) {\n // If we already threw an error, re-throw it\n if ((parseError as VerifyMagicLinkError)?.code === \"expired\") {\n throw parseError;\n }\n // Otherwise fall through to invalid_code\n }\n\n const error: VerifyMagicLinkError = {\n code: \"invalid_code\",\n message: \"Incorrect code, check your email and try again.\"\n };\n throw error;\n }\n const error: VerifyMagicLinkError = {\n code: \"unknown\",\n message: `Magic link verification failed (${response.status} ${response.statusText})`\n };\n throw error;\n }\n\n const payload = await response.json();\n const token = payload?.token ?? payload?.jwt ?? payload?.access_token;\n if (typeof token !== \"string\") {\n throw new Error(\"Magic link verification succeeded but no token returned\");\n }\n\n return { token, raw: payload };\n }\n});\n\nexport interface CustomBrandingResponse {\n brandingData: string;\n documentation: unknown;\n}\n\nexport interface PortalLicenseField {\n key: string;\n label: string;\n value: string | null;\n isSecret?: boolean;\n}\n\nexport interface PortalLicenseDetails {\n id?: string;\n status?: string;\n statusLabel?: string;\n environment?: string;\n expiresAt?: string | null;\n releaseChannels?: string[];\n installMethods?: string[];\n installNotes?: string;\n customerName?: string;\n customerId?: string;\n customerOrganization?: string;\n fields: PortalLicenseField[];\n}\n\nexport interface ListSupportBundlesInput {\n token: string;\n}\n\nexport interface SupportBundleInsight {\n level: string;\n primary: string;\n key?: string;\n detail?: string;\n}\n\nexport interface SupportBundleSummary {\n id: string;\n createdAt?: string;\n status?: string;\n size?: number;\n instanceId?: string;\n insights?: SupportBundleInsight[];\n metadata?: Record<string, unknown>;\n}\n\nexport interface ListSupportBundlesResult {\n bundles: SupportBundleSummary[];\n totalCount: number;\n raw: unknown;\n}\n\nexport interface DownloadSupportBundleInput {\n token: string;\n bundleId: string;\n}\n\nexport interface DownloadSupportBundleResult {\n signedUrl: string;\n}\n\nexport interface DeleteSupportBundleInput {\n token: string;\n bundleId: string;\n}\n\nexport interface DeleteSupportBundleResult {\n success: boolean;\n}\n\nexport interface UploadSupportBundleInput {\n token: string;\n appId: string;\n}\n\nexport interface UploadSupportBundleResult {\n uploadUrl: string;\n appId: string;\n}\n\nexport interface UploadSupportBundleCompleteInput {\n token: string;\n appId: string;\n fileContent: ArrayBuffer;\n contentLength: number;\n}\n\nexport interface UploadSupportBundleCompleteResult {\n bundleId: string;\n slug: string;\n}\n\nexport interface FetchLicenseDetailsInput {\n token: string;\n}\n\nexport interface FetchLicenseDetailsResult {\n license: PortalLicenseDetails;\n raw: unknown;\n}\n\nexport interface FetchInstallOptionsInput {\n token: string;\n}\n\nexport interface FetchInstallOptionsResult {\n showLinux: boolean;\n showHelm: boolean;\n}\n\nexport interface FetchLicenseSummaryInput {\n token: string;\n}\n\nexport interface FetchLicenseSummaryResult {\n type: string;\n expiresAt: string | null;\n}\n\nexport interface FetchCustomersInput {\n token: string;\n}\n\nexport interface Customer {\n id: string;\n name: string;\n licenseId: string;\n licenseType: string;\n expiresAt: string;\n isEnterprisePortalEnabled: boolean;\n}\n\nexport interface FetchCustomersResult {\n customers: Customer[];\n}\n\nexport interface SwitchCustomerInput {\n token: string;\n customerId: string;\n}\n\nexport interface SwitchCustomerResult {\n token: string;\n}\n\nexport interface ListReleasesInput {\n token: string;\n}\n\nexport interface ListReleasesResult {\n status: number;\n body: string | null;\n}\n\n/**\n * Internal implementation of fetchCustomBranding.\n * Wrapped with React's cache() to deduplicate calls within a single request.\n *\n * Updated to use the new Enterprise Portal API /enterprise-portal/public/branding endpoint\n * which returns clean, non-base64 encoded data.\n */\nconst fetchCustomBrandingImpl = async (): Promise<CustomBrandingResponse> => {\n const appSlug = process.env.PORTAL_APP_SLUG;\n\n if (!appSlug) {\n throw new Error(\"PORTAL_APP_SLUG is not configured\");\n }\n\n const url = `${getApiOrigin()}/enterprise-portal/public/branding?app_slug=${encodeURIComponent(\n appSlug\n )}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\n \"[portal-components] fetching custom branding via %s (Enterprise Portal API)\",\n url\n );\n }\n\n const response = await fetch(url, {\n headers: {\n accept: \"application/json\"\n }\n });\n\n if (!response.ok) {\n throw new Error(\n `Custom branding request failed (${response.status} ${response.statusText})`\n );\n }\n\n const payload = await response.json();\n\n // New Enterprise Portal API returns clean data, not base64 encoded\n // Map the new field names to the old structure for compatibility\n const brandingObject = {\n logo: payload.logoUrl,\n title: payload.appName,\n customColor1: payload.primaryColor,\n customColor2: payload.secondaryColor,\n favicon: payload.faviconUrl,\n };\n\n // Encode to base64 to maintain compatibility with existing decodeBranding function\n const brandingData = Buffer.from(JSON.stringify(brandingObject)).toString(\"base64\");\n\n return {\n brandingData,\n documentation: null // Documentation not included in new API's public endpoint\n };\n};\n\n/**\n * Fetches custom branding for the portal.\n * This function is cached per-request to avoid duplicate API calls when called\n * from multiple components (e.g., TopNav and page components).\n */\nexport const fetchCustomBranding = cache(fetchCustomBrandingImpl);\n\nexport const decodeJwtPayload = (token: string): Record<string, unknown> => {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n throw new Error(\"Invalid JWT received\");\n }\n\n const payloadSegment = parts[1];\n if (!payloadSegment) {\n throw new Error(\"JWT payload segment missing\");\n }\n\n const padded = payloadSegment.padEnd(\n payloadSegment.length + ((4 - (payloadSegment.length % 4)) % 4),\n \"=\"\n );\n const decoded = Buffer.from(padded, \"base64\").toString(\"utf-8\");\n return JSON.parse(decoded) as Record<string, unknown>;\n};\n\n/**\n * Extracts customer ID from JWT token. Throws if extraction fails.\n */\nexport const getCustomerIdFromToken = (token: string): string => {\n const payload = decodeJwtPayload(token);\n const customerId = payload?.customer_id || payload?.customerId;\n if (typeof customerId !== \"string\" || !customerId.trim()) {\n throw new Error(\"Unable to determine customer_id from session token\");\n }\n return customerId.trim();\n};\n\nconst resolveSupportBundlesEndpoint = () => {\n const fallback = `${getApiOrigin()}/v3/supportbundles`;\n const explicit = process.env.SUPPORT_BUNDLES_ENDPOINT;\n\n if (!explicit) {\n return new URL(fallback);\n }\n\n try {\n return new URL(explicit);\n } catch (error) {\n console.warn(\n `[portal-components] invalid SUPPORT_BUNDLES_ENDPOINT, using fallback`,\n error\n );\n return new URL(fallback);\n }\n};\n\nexport const listSupportBundles = defineServerAction<\n ListSupportBundlesInput,\n ListSupportBundlesResult\n>({\n id: \"support/list-bundles\",\n description:\n \"Fetches support bundles associated with the customer found in the portal session JWT.\",\n visibility: \"customer\",\n tags: [\"support\", \"bundles\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Support bundle listing requires a session token\");\n }\n\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n const url = `${getApiOrigin()}/enterprise-portal/support-bundles`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching support bundles via %s (Enterprise Portal API)\", url);\n }\n\n const response = await authenticatedFetch(url, {\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (context?.signal?.aborted) {\n throw new Error(\"Support bundles request was aborted\");\n }\n\n if (!response.ok) {\n throw new Error(\n `Support bundles request failed (${response.status} ${response.statusText})`\n );\n }\n\n const payload = await response.json();\n // Extract from Enterprise Portal API envelope\n const raw = payload.data;\n\n const rawRecord =\n raw && typeof raw === \"object\" ? (raw as Record<string, unknown>) : undefined;\n\n const parseInsights = (raw: unknown): SupportBundleInsight[] | undefined => {\n if (!Array.isArray(raw)) return undefined;\n return raw\n .filter((i): i is Record<string, unknown> => i && typeof i === \"object\")\n .map((i) => ({\n level: String(i.level ?? \"\"),\n primary: String(i.primary ?? \"\"),\n key: typeof i.key === \"string\" ? i.key : undefined,\n detail: typeof i.detail === \"string\" ? i.detail : undefined\n }));\n };\n\n const bundles: SupportBundleSummary[] = Array.isArray(\n rawRecord?.supportBundles\n )\n ? (rawRecord?.supportBundles as unknown[]).map((item) => {\n if (!item || typeof item !== \"object\") {\n return {\n id: \"\",\n createdAt: undefined,\n status: undefined,\n size: undefined,\n instanceId: undefined,\n insights: undefined,\n metadata: undefined\n };\n }\n\n const record = item as Record<string, unknown>;\n return {\n id: String(record.id ?? \"\"),\n createdAt:\n typeof record.createdAt === \"string\"\n ? (record.createdAt as string)\n : undefined,\n status:\n typeof record.status === \"string\"\n ? (record.status as string)\n : undefined,\n size:\n typeof record.size === \"number\"\n ? record.size\n : undefined,\n instanceId:\n typeof record.instanceId === \"string\"\n ? record.instanceId\n : undefined,\n insights: parseInsights(record.insights),\n metadata: record\n };\n })\n : Array.isArray(raw)\n ? (raw as unknown[]).map((item) => {\n if (!item || typeof item !== \"object\") {\n return {\n id: \"\",\n createdAt: undefined,\n status: undefined,\n size: undefined,\n instanceId: undefined,\n insights: undefined,\n metadata: undefined\n };\n }\n const record = item as Record<string, unknown>;\n return {\n id: String(record.id ?? \"\"),\n createdAt:\n typeof record.createdAt === \"string\"\n ? (record.createdAt as string)\n : undefined,\n status:\n typeof record.status === \"string\"\n ? (record.status as string)\n : undefined,\n size:\n typeof record.size === \"number\"\n ? record.size\n : undefined,\n instanceId:\n typeof record.instanceId === \"string\"\n ? record.instanceId\n : undefined,\n insights: parseInsights(record.insights),\n metadata: record\n };\n })\n : [];\n\n const totalCount = (() => {\n if (rawRecord) {\n if (\n typeof rawRecord.totalCount === \"number\" &&\n Number.isFinite(rawRecord.totalCount)\n ) {\n return rawRecord.totalCount;\n }\n if (Array.isArray(rawRecord.supportBundles)) {\n return rawRecord.supportBundles.length;\n }\n }\n\n if (Array.isArray(raw)) {\n return raw.length;\n }\n\n return bundles.length;\n })();\n\n return {\n bundles,\n totalCount,\n raw\n };\n }\n});\n\nexport const downloadSupportBundle = defineServerAction<\n DownloadSupportBundleInput,\n DownloadSupportBundleResult\n>({\n id: \"support/download-bundle\",\n description: \"Gets a signed URL for downloading a support bundle.\",\n visibility: \"customer\",\n tags: [\"support\", \"bundles\", \"download\"],\n async run({ token, bundleId }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Support bundle download requires a session token\");\n }\n\n if (!bundleId || typeof bundleId !== \"string\") {\n throw new Error(\"Support bundle download requires a bundle ID\");\n }\n\n // NOTE: customerId is still required in query params because this endpoint\n // delegates to v3 handler which validates customer_id\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/enterprise-portal/support-bundles/${encodeURIComponent(bundleId)}/download?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] getting support bundle download URL via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n throw new Error(\n `Support bundle download URL request failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n const data = await response.json();\n const signedUrl = data?.signedUrl;\n\n if (typeof signedUrl !== \"string\" || !signedUrl) {\n throw new Error(\"Support bundle download response missing signedUrl\");\n }\n\n return { signedUrl };\n }\n});\n\nexport const deleteSupportBundle = defineServerAction<\n DeleteSupportBundleInput,\n DeleteSupportBundleResult\n>({\n id: \"support/delete-bundle\",\n description: \"Deletes a support bundle.\",\n visibility: \"customer\",\n tags: [\"support\", \"bundles\", \"delete\"],\n async run({ token, bundleId }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Support bundle deletion requires a session token\");\n }\n\n if (!bundleId || typeof bundleId !== \"string\") {\n throw new Error(\"Support bundle deletion requires a bundle ID\");\n }\n\n // NOTE: customerId is still required in query params because this endpoint\n // delegates to v3 handler which validates customer_id\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/enterprise-portal/support-bundles/${encodeURIComponent(bundleId)}?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] deleting support bundle via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"DELETE\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n if (response.status === 404) {\n throw new Error(\"Support bundle not found\");\n }\n throw new Error(\n `Support bundle deletion failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n return { success: true };\n }\n});\n\nexport const uploadSupportBundle = defineServerAction<\n UploadSupportBundleCompleteInput,\n UploadSupportBundleCompleteResult\n>({\n id: \"support/upload-bundle\",\n description: \"Uploads a support bundle to the server.\",\n visibility: \"customer\",\n tags: [\"support\", \"bundles\", \"upload\"],\n async run({ token, appId, fileContent, contentLength }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Support bundle upload requires a session token\");\n }\n\n if (!appId || typeof appId !== \"string\") {\n throw new Error(\"Support bundle upload requires an app ID\");\n }\n\n if (!fileContent || !(fileContent instanceof ArrayBuffer)) {\n throw new Error(\"Support bundle upload requires file content\");\n }\n\n const endpoint = `${getApiOrigin()}/enterprise-portal/support-bundles/upload/${encodeURIComponent(appId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] uploading support bundle via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"POST\",\n token,\n headers: {\n \"content-type\": \"application/gzip\",\n \"content-length\": String(contentLength)\n },\n body: fileContent,\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n throw new Error(\n `Support bundle upload failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n const data = await response.json();\n\n return {\n bundleId: data?.bundleId ?? data?.bundle_id ?? \"\",\n slug: data?.slug ?? \"\"\n };\n }\n});\n\n/**\n * Helper to get the upload endpoint URL for client-side uploads with progress tracking.\n * Use this when you need progress indication - call this to get the URL, then upload directly from client.\n */\nexport const getSupportBundleUploadUrl = (appId: string): string => {\n return `${getApiOrigin()}/enterprise-portal/support-bundles/upload/${encodeURIComponent(appId)}`;\n};\n\nexport const listReleases = defineServerAction<\n ListReleasesInput,\n ListReleasesResult\n>({\n id: \"releases/list\",\n description: \"Lists available releases for the authenticated customer.\",\n visibility: \"customer\",\n tags: [\"releases\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"List releases requires a session token\");\n }\n\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n const endpoint = `${getApiOrigin()}/enterprise-portal/releases`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching releases via %s (Enterprise Portal API)\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n const bodyText = await response\n .text()\n .catch((error) => {\n console.warn(\"[portal-components] listReleases read error\", error);\n return null;\n });\n\n if (!response.ok) {\n throw new Error(\n `List releases request failed (${response.status} ${response.statusText})`\n );\n }\n\n return {\n status: response.status,\n body: bodyText\n };\n }\n});\n\nconst asRecord = (value: unknown): Record<string, unknown> | undefined => {\n if (value && typeof value === \"object\") {\n return value as Record<string, unknown>;\n }\n return undefined;\n};\n\nconst getValue = (\n record: Record<string, unknown> | undefined,\n key: string\n) => (record ? record[key] : undefined);\n\nconst getString = (\n record: Record<string, unknown> | undefined,\n key: string\n): string | undefined => {\n const value = getValue(record, key);\n return typeof value === \"string\" ? value : undefined;\n};\n\nconst getBoolean = (\n record: Record<string, unknown> | undefined,\n key: string\n): boolean | undefined => {\n const value = getValue(record, key);\n if (typeof value === \"boolean\") {\n return value;\n }\n if (typeof value === \"number\") {\n return value === 1;\n }\n if (typeof value === \"string\") {\n const normalized = value.trim().toLowerCase();\n if ([\"true\", \"1\", \"yes\"].includes(normalized)) {\n return true;\n }\n if ([\"false\", \"0\", \"no\"].includes(normalized)) {\n return false;\n }\n }\n return undefined;\n};\n\nconst toDisplayValue = (value: unknown): string | null => {\n if (value === null || value === undefined) {\n return null;\n }\n if (typeof value === \"string\") {\n return value;\n }\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n};\n\nconst normalizeStringArray = (value: unknown): string[] | undefined => {\n if (Array.isArray(value)) {\n const normalized = value\n .map((item) =>\n typeof item === \"string\" ? item.trim() : \"\"\n )\n .filter((item) => item.length > 0);\n return normalized.length ? normalized : undefined;\n }\n\n if (typeof value === \"string\") {\n const normalized = value\n .split(\",\")\n .map((item) => item.trim())\n .filter((item) => item.length > 0);\n return normalized.length ? normalized : undefined;\n }\n\n return undefined;\n};\n\nconst normalizeLicenseFields = (input: unknown): PortalLicenseField[] => {\n if (!input) {\n return [];\n }\n\n if (Array.isArray(input)) {\n return input\n .map((field, index) => {\n if (!field || typeof field !== \"object\") {\n return null;\n }\n const candidate = field as Record<string, unknown>;\n const key =\n typeof candidate.key === \"string\" && candidate.key.trim().length\n ? candidate.key.trim()\n : typeof candidate.name === \"string\" && candidate.name.trim().length\n ? candidate.name.trim()\n : typeof candidate.label === \"string\" && candidate.label.trim().length\n ? candidate.label.trim()\n : `field-${index}`;\n const label =\n typeof candidate.label === \"string\" && candidate.label.trim().length\n ? candidate.label.trim()\n : typeof candidate.name === \"string\" && candidate.name.trim().length\n ? candidate.name.trim()\n : key;\n let value: unknown =\n candidate.value ?? candidate.data ?? candidate.content;\n if (\n (value === undefined || value === null) &&\n typeof candidate.text === \"string\"\n ) {\n value = candidate.text;\n }\n if (\n (value === undefined || value === null) &&\n typeof candidate.defaultValue === \"string\"\n ) {\n value = candidate.defaultValue;\n }\n const isSecret = Boolean(\n candidate.isSecret ?? candidate.secret ?? candidate.masked\n );\n const resolved = toDisplayValue(value);\n return {\n key,\n label,\n value: resolved,\n isSecret\n } as PortalLicenseField;\n })\n .filter((field): field is PortalLicenseField => Boolean(field));\n }\n\n if (typeof input === \"object\") {\n return Object.entries(input as Record<string, unknown>).map(\n ([key, value]) => {\n let resolvedValue: unknown = value;\n let isSecret = false;\n if (value && typeof value === \"object\") {\n const obj = value as Record<string, unknown>;\n if (\"value\" in obj) {\n resolvedValue = obj.value;\n }\n isSecret = Boolean(obj.isSecret ?? obj.secret ?? obj.masked);\n }\n const normalized = toDisplayValue(resolvedValue);\n return {\n key,\n label: key,\n value: normalized,\n isSecret\n };\n }\n );\n }\n\n return [];\n};\n\nconst extractChannelNames = (input: unknown): string[] | undefined => {\n if (!Array.isArray(input)) {\n return undefined;\n }\n const names = input\n .map((item) => {\n if (typeof item === \"string\") {\n return item.trim();\n }\n const record = asRecord(item);\n if (!record) {\n return null;\n }\n return (\n getString(record, \"name\") ??\n getString(record, \"channelName\") ??\n getString(record, \"channel\") ??\n getString(record, \"channelSlug\") ??\n getString(record, \"slug\") ??\n undefined\n );\n })\n .filter((name): name is string => Boolean(name && name.length));\n return names.length ? names : undefined;\n};\n\nconst normalizeEntitlementFields = (\n fieldsInput: unknown,\n valuesInput: unknown\n): PortalLicenseField[] => {\n const valuesMap = new Map<string, string | null>();\n const assignValue = (key: string | undefined, value: unknown) => {\n if (!key) {\n return;\n }\n valuesMap.set(key, toDisplayValue(value));\n };\n\n if (Array.isArray(valuesInput)) {\n valuesInput.forEach((item) => {\n const record = asRecord(item);\n if (!record) {\n if (typeof item === \"string\") {\n assignValue(item, item);\n }\n return;\n }\n const key =\n getString(record, \"name\") ??\n getString(record, \"field\") ??\n getString(record, \"title\") ??\n getString(record, \"label\") ??\n getString(record, \"slug\") ??\n (() => {\n const idValue = getValue(record, \"id\");\n if (typeof idValue === \"string\" || typeof idValue === \"number\") {\n return String(idValue);\n }\n return undefined;\n })();\n const value =\n getValue(record, \"value\") ??\n getValue(record, \"currentValue\") ??\n getValue(record, \"entitlementValue\") ??\n getValue(record, \"content\") ??\n getValue(record, \"data\") ??\n getValue(record, \"defaultVal\") ??\n getValue(record, \"defaultValue\");\n assignValue(key, value);\n });\n } else if (valuesInput && typeof valuesInput === \"object\") {\n Object.entries(valuesInput as Record<string, unknown>).forEach(\n ([key, value]) => assignValue(key, value)\n );\n }\n\n const normalized: PortalLicenseField[] = [];\n\n if (Array.isArray(fieldsInput)) {\n fieldsInput.forEach((item, index) => {\n const record = asRecord(item);\n if (!record) {\n return;\n }\n const baseKey =\n getString(record, \"name\") ??\n getString(record, \"field\") ??\n getString(record, \"slug\") ??\n `entitlement-${index}`;\n const key = `entitlement-${baseKey}`;\n const label =\n getString(record, \"title\") ??\n getString(record, \"label\") ??\n baseKey;\n const defaultValue =\n getString(record, \"defaultVal\") ??\n getString(record, \"default\") ??\n getString(record, \"defaultValue\");\n const value =\n valuesMap.get(baseKey) ??\n valuesMap.get(label) ??\n defaultValue ??\n null;\n const isSecret = Boolean(\n getBoolean(record, \"secret\") ??\n getBoolean(record, \"isSecret\") ??\n getBoolean(record, \"masked\")\n );\n normalized.push({\n key,\n label,\n value,\n isSecret\n });\n });\n }\n\n valuesMap.forEach((value, key) => {\n const normalizedKey = `entitlement-${key}`;\n if (!normalized.some((field) => field.key === normalizedKey)) {\n normalized.push({\n key: normalizedKey,\n label: key,\n value\n });\n }\n });\n\n return normalized;\n};\n\nconst normalizeLicensePayload = (payload: unknown): PortalLicenseDetails => {\n const payloadRecord = asRecord(payload);\n const rootRecord =\n asRecord(getValue(payloadRecord, \"license\")) ??\n asRecord(getValue(payloadRecord, \"data\")) ??\n payloadRecord ??\n ({} as Record<string, unknown>);\n const sourceRecord =\n asRecord(getValue(rootRecord, \"metadata\")) ?? rootRecord;\n\n const customer =\n asRecord(getValue(rootRecord, \"customer\")) ??\n asRecord(getValue(sourceRecord, \"customer\")) ??\n asRecord(getValue(payloadRecord, \"customer\")) ??\n ({} as Record<string, unknown>);\n\n let releaseChannels =\n normalizeStringArray(\n getValue(rootRecord, \"releaseChannels\") ??\n getValue(sourceRecord, \"releaseChannels\") ??\n getValue(sourceRecord, \"channels\") ??\n getValue(rootRecord, \"channels\") ??\n getValue(sourceRecord, \"channel\") ??\n getValue(rootRecord, \"channel\")\n ) ?? undefined;\n\n if (!releaseChannels) {\n releaseChannels =\n extractChannelNames(getValue(rootRecord, \"channels\")) ??\n extractChannelNames(getValue(sourceRecord, \"channels\")) ??\n undefined;\n }\n\n let installMethods =\n normalizeStringArray(\n getValue(rootRecord, \"installMethods\") ??\n getValue(sourceRecord, \"installMethods\") ??\n getValue(sourceRecord, \"install_options\") ??\n getValue(rootRecord, \"install_options\") ??\n getValue(sourceRecord, \"installOptions\")\n ) ?? undefined;\n\n if (!installMethods || installMethods.length === 0) {\n const resolved: string[] = [];\n const flag = (key: string) =>\n getBoolean(rootRecord, key) ?? getBoolean(sourceRecord, key) ?? false;\n\n if (flag(\"isKotsInstallEnabled\")) {\n resolved.push(\"Replicated KOTS\");\n }\n if (flag(\"isHelmInstallEnabled\")) {\n resolved.push(\"Helm\");\n }\n if (flag(\"isHelmAirgapEnabled\")) {\n resolved.push(\"Helm Airgap\");\n }\n if (\n flag(\"isEmbeddedClusterDownloadEnabled\") ||\n flag(\"isEmbeddedClusterMultiNodeEnabled\")\n ) {\n resolved.push(\"Embedded Cluster\");\n }\n if (flag(\"isKurlInstallEnabled\")) {\n resolved.push(\"kURL\");\n }\n if (flag(\"isGitopsSupported\")) {\n resolved.push(\"GitOps\");\n }\n if (resolved.length) {\n installMethods = Array.from(new Set(resolved));\n }\n }\n\n const expiresAtSource =\n getValue(sourceRecord, \"expiresAt\") ??\n getValue(sourceRecord, \"expireAt\") ??\n getValue(sourceRecord, \"expire_at\") ??\n getValue(sourceRecord, \"expiration\") ??\n getValue(sourceRecord, \"expirationDate\") ??\n getValue(sourceRecord, \"expires_on\") ??\n getValue(rootRecord, \"expiresAt\") ??\n getValue(rootRecord, \"expireAt\") ??\n getValue(rootRecord, \"expire_at\") ??\n getValue(rootRecord, \"expiration\");\n\n const expiresAt =\n typeof expiresAtSource === \"string\" && expiresAtSource.trim().length\n ? expiresAtSource\n : expiresAtSource === null\n ? null\n : undefined;\n\n const baseFields = normalizeLicenseFields(\n getValue(rootRecord, \"additionalFields\") ??\n getValue(sourceRecord, \"additionalFields\") ??\n getValue(sourceRecord, \"fields\") ??\n getValue(rootRecord, \"fields\") ??\n getValue(payloadRecord, \"fields\") ??\n getValue(payloadRecord, \"additional_fields\")\n );\n\n const entitlementFields = normalizeEntitlementFields(\n getValue(rootRecord, \"entitlementFields\") ??\n getValue(sourceRecord, \"entitlementFields\"),\n getValue(rootRecord, \"entitlementValues\") ??\n getValue(sourceRecord, \"entitlementValues\")\n );\n\n const fields = [\n ...baseFields,\n ...entitlementFields.filter(\n (field) => !baseFields.some((existing) => existing.key === field.key)\n )\n ];\n\n const statusFromSource =\n getString(sourceRecord, \"status\") ??\n getString(sourceRecord, \"state\");\n const statusLabelFromSource =\n getString(sourceRecord, \"statusLabel\") ??\n getString(sourceRecord, \"stateLabel\");\n const expiredFlag =\n getBoolean(sourceRecord, \"isExpired\") ??\n getBoolean(rootRecord, \"isExpired\");\n const derivedStatus =\n statusFromSource ??\n (typeof expiredFlag === \"boolean\"\n ? expiredFlag\n ? \"expired\"\n : \"active\"\n : undefined);\n const statusLabel =\n statusLabelFromSource ??\n (derivedStatus\n ? derivedStatus.charAt(0).toUpperCase() + derivedStatus.slice(1)\n : undefined);\n\n const licenseType =\n getString(sourceRecord, \"licenseType\") ??\n getString(rootRecord, \"licenseType\");\n\n const status = derivedStatus;\n\n const license: PortalLicenseDetails = {\n id:\n getString(rootRecord, \"id\") ??\n getString(sourceRecord, \"id\") ??\n getString(sourceRecord, \"licenseId\") ??\n getString(customer, \"licenseId\") ??\n undefined,\n status,\n statusLabel,\n environment:\n getString(sourceRecord, \"environment\") ??\n getString(sourceRecord, \"tier\") ??\n licenseType ??\n undefined,\n expiresAt: expiresAt ?? null,\n releaseChannels: releaseChannels ?? [\n getString(rootRecord, \"channelName\") ??\n getString(rootRecord, \"channel\") ??\n undefined\n ].filter((value): value is string => Boolean(value)),\n installMethods,\n installNotes: getString(sourceRecord, \"installNotes\"),\n customerName:\n getString(sourceRecord, \"customerName\") ??\n getString(customer, \"name\") ??\n undefined,\n customerId:\n getString(sourceRecord, \"customerId\") ??\n getString(customer, \"id\") ??\n getString(rootRecord, \"customerId\") ??\n undefined,\n customerOrganization:\n getString(customer, \"organization\") ??\n getString(sourceRecord, \"customerOrganization\") ??\n getString(rootRecord, \"customerOrganization\") ??\n undefined,\n fields\n };\n\n return license;\n};\n\nexport const fetchLicenseDetails = defineServerAction<\n FetchLicenseDetailsInput,\n FetchLicenseDetailsResult\n>({\n id: \"license/fetch-details\",\n description: \"Fetches the authenticated user's enterprise license details.\",\n visibility: \"customer\",\n tags: [\"license\", \"entitlements\"],\n async run({ token }, context) {\n if (typeof token !== \"string\" || token.trim().length === 0) {\n throw new Error(\"fetchLicenseDetails requires a non-empty token\");\n }\n\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n const endpoint = `${getApiOrigin()}/enterprise-portal/license`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching license via %s (Enterprise Portal API)\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `License request failed (${response.status} ${response.statusText})`\n );\n }\n\n const payload = await response.json();\n\n // Extract from Enterprise Portal API envelope\n const licenseData = payload.data;\n const license = normalizeLicensePayload(licenseData);\n\n return {\n license,\n raw: licenseData ?? null\n };\n }\n});\n\nexport const fetchInstallOptions = defineServerAction<\n FetchInstallOptionsInput,\n FetchInstallOptionsResult\n>({\n id: \"license/fetch-install-options\",\n description: \"Fetches install options based on license entitlements.\",\n visibility: \"customer\",\n tags: [\"license\", \"install\"],\n async run({ token }, context) {\n if (typeof token !== \"string\" || token.trim().length === 0) {\n throw new Error(\"fetchInstallOptions requires a non-empty token\");\n }\n\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n const endpoint = `${getApiOrigin()}/enterprise-portal/license`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching install options via %s (Enterprise Portal API)\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `License request failed (${response.status} ${response.statusText})`\n );\n }\n\n const envelope = await response.json();\n const licenseData = envelope.data;\n\n // Check for embedded cluster (Linux) and Helm install flags\n const getBoolean = (obj: unknown, key: string): boolean => {\n if (obj && typeof obj === \"object\" && key in obj) {\n const val = (obj as Record<string, unknown>)[key];\n return val === true || val === \"true\";\n }\n return false;\n };\n\n const showLinux = getBoolean(licenseData, \"isEmbeddedClusterDownloadEnabled\");\n const showHelm = getBoolean(licenseData, \"isHelmInstallEnabled\");\n\n return {\n showLinux,\n showHelm\n };\n }\n});\n\nexport const fetchLicenseSummary = defineServerAction<\n FetchLicenseSummaryInput,\n FetchLicenseSummaryResult\n>({\n id: \"license/fetch-summary\",\n description: \"Fetches license summary for the license card.\",\n visibility: \"customer\",\n tags: [\"license\"],\n async run({ token }, context) {\n if (typeof token !== \"string\" || token.trim().length === 0) {\n throw new Error(\"fetchLicenseSummary requires a non-empty token\");\n }\n\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n const endpoint = `${getApiOrigin()}/enterprise-portal/license`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching license summary via %s (Enterprise Portal API)\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `License request failed (${response.status} ${response.statusText})`\n );\n }\n\n const envelope = await response.json();\n const licenseData = envelope.data;\n const license = normalizeLicensePayload(licenseData);\n\n // Extract type and expiration\n const type = license.environment || \"Unknown\";\n const expiresAt = license.expiresAt || null;\n\n return {\n type,\n expiresAt\n };\n }\n});\n\nexport const fetchCustomers = defineServerAction<\n FetchCustomersInput,\n FetchCustomersResult\n>({\n id: \"auth/fetch-customers\",\n description: \"Fetches the list of customers/teams for the authenticated user.\",\n visibility: \"customer\",\n tags: [\"auth\", \"customers\"],\n async run({ token }, context) {\n if (typeof token !== \"string\" || token.trim().length === 0) {\n throw new Error(\"fetchCustomers requires a non-empty token\");\n }\n\n // NEW: Use Enterprise Portal API /user endpoint (returns customers + user profile)\n const endpoint = `${getApiOrigin()}/enterprise-portal/user`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching customers via %s (Enterprise Portal API)\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Fetch customers request failed (${response.status} ${response.statusText})`\n );\n }\n\n const envelope = await response.json();\n const userData = envelope.data;\n\n return {\n customers: userData?.customers || []\n };\n }\n});\n\nexport const switchCustomer = defineServerAction<\n SwitchCustomerInput,\n SwitchCustomerResult\n>({\n id: \"auth/switch-customer\",\n description: \"Switches the JWT to a different customer/team.\",\n visibility: \"customer\",\n tags: [\"auth\", \"customers\"],\n async run({ token, customerId }, context) {\n if (typeof token !== \"string\" || token.trim().length === 0) {\n throw new Error(\"switchCustomer requires a non-empty token\");\n }\n \n if (typeof customerId !== \"string\" || customerId.trim().length === 0) {\n throw new Error(\"switchCustomer requires a non-empty customerId\");\n }\n\n // NEW: Use Enterprise Portal API auth endpoint\n const endpoint = `${getApiOrigin()}/enterprise-portal/auth/switch-team`;\n\n const requestBody = { customer_id: customerId };\n\n const response = await authenticatedFetch(endpoint, {\n method: \"POST\",\n token,\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify(requestBody),\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('[portal-components] switchCustomer error response:', errorText);\n throw new Error(\n `Switch customer request failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n const payload = await response.json();\n\n // API returns 'jwt' field, not 'token'\n const newToken = payload.jwt || payload.token || token;\n \n return {\n token: newToken\n };\n }\n});\n\n// =============================================================================\n// Security Types\n// =============================================================================\n\nexport type SecurityInstallType = \"linux\" | \"helm\";\n\nexport interface SecurityScanSummary {\n critical: Record<string, string>;\n high: Record<string, string>;\n medium: Record<string, string>;\n low: Record<string, string>;\n}\n\nexport interface SecurityScanWrapper {\n input: string;\n digest?: string;\n last_scanned_at?: string;\n result: SecurityScanSummary;\n not_found?: boolean;\n}\n\nexport interface SecurityReleaseImage {\n image: string;\n sha: string;\n size: number;\n platforms: { os: string; architecture: string }[];\n security?: SecurityScanWrapper;\n}\n\nexport interface GetSecurityInfoInput {\n token: string;\n installType: SecurityInstallType;\n channelSequence: number;\n isAirgap?: boolean;\n}\n\nexport interface GetSecurityInfoResult {\n images: SecurityReleaseImage[];\n}\n\nexport interface SecurityInfoDiff {\n oldTags: string[];\n newTags: string[];\n oldVulns?: SecurityScanSummary;\n newVulns?: SecurityScanSummary;\n added?: SecurityScanSummary;\n removed?: SecurityScanSummary;\n}\n\nexport interface GetSecurityInfoDiffInput {\n token: string;\n installType: SecurityInstallType;\n fromChannelSequence: number;\n toChannelSequence: number;\n isAirgap?: boolean;\n}\n\nexport interface GetSecurityInfoDiffResult {\n from_channel_sequence: number;\n to_channel_sequence: number;\n images: Record<string, SecurityInfoDiff>;\n}\n\nexport interface UnifiedSbom {\n sbom: string;\n sbom_source: string;\n}\n\nexport interface SpdxCreationInfo {\n created: string;\n creators: string[];\n}\n\nexport interface SpdxDocument {\n SPDXID: string;\n spdxVersion: string;\n name: string;\n creationInfo?: SpdxCreationInfo;\n packages?: unknown[];\n files?: unknown[];\n}\n\nexport interface GetSecurityInfoSBOMInput {\n token: string;\n installType: SecurityInstallType;\n channelSequence: number;\n isAirgap?: boolean;\n unifiedSbom?: boolean;\n}\n\nexport interface GetSecurityInfoSBOMResult {\n sboms: {\n unified?: UnifiedSbom;\n };\n}\n\n// =============================================================================\n// Security Actions\n// =============================================================================\n\n/**\n * Fetches security scan (CVE) information for a specific release.\n */\nexport const getSecurityInfo = defineServerAction<\n GetSecurityInfoInput,\n GetSecurityInfoResult\n>({\n id: \"security/get-info\",\n description: \"Fetches CVE security scan results for a specific release\",\n visibility: \"customer\",\n tags: [\"security\", \"cve\"],\n async run({ token, installType, channelSequence, isAirgap = false }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Security info request requires a session token\");\n }\n\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n const params = new URLSearchParams({\n install_type: installType,\n channel_sequence: channelSequence.toString(),\n is_airgap: isAirgap.toString()\n });\n\n const url = `${getApiOrigin()}/enterprise-portal/security?${params.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching security info via %s (Enterprise Portal API)\", url);\n }\n\n const response = await authenticatedFetch(url, {\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Security info request failed (${response.status} ${response.statusText})`\n );\n }\n\n const payload = await response.json();\n // Extract from Enterprise Portal API envelope\n return payload.data as GetSecurityInfoResult;\n }\n});\n\n/**\n * Fetches security diff between two releases (fixed/added CVEs).\n */\nexport const getSecurityInfoDiff = defineServerAction<\n GetSecurityInfoDiffInput,\n GetSecurityInfoDiffResult\n>({\n id: \"security/get-info-diff\",\n description: \"Fetches CVE diff between two releases showing fixed and added vulnerabilities\",\n visibility: \"customer\",\n tags: [\"security\", \"cve\", \"diff\"],\n async run({ token, installType, fromChannelSequence, toChannelSequence, isAirgap = false }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Security info diff request requires a session token\");\n }\n\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n const params = new URLSearchParams({\n install_type: installType,\n from_channel_sequence: fromChannelSequence.toString(),\n to_channel_sequence: toChannelSequence.toString(),\n is_airgap: isAirgap.toString()\n });\n\n const url = `${getApiOrigin()}/enterprise-portal/security-diff?${params.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching security info diff via %s\", url);\n }\n\n const response = await authenticatedFetch(url, {\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Security info diff request failed (${response.status} ${response.statusText})`\n );\n }\n\n const envelope = await response.json();\n // Extract from Enterprise Portal API envelope\n return envelope.data as GetSecurityInfoDiffResult;\n }\n});\n\n/**\n * Fetches SBOM (Software Bill of Materials) for a specific release.\n */\nexport const getSecurityInfoSBOM = defineServerAction<\n GetSecurityInfoSBOMInput,\n GetSecurityInfoSBOMResult\n>({\n id: \"security/get-sbom\",\n description: \"Fetches Software Bill of Materials (SBOM) for a specific release\",\n visibility: \"customer\",\n tags: [\"security\", \"sbom\"],\n async run({ token, installType, channelSequence, isAirgap = false, unifiedSbom = true }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Security SBOM request requires a session token\");\n }\n\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n const params = new URLSearchParams({\n install_type: installType,\n channel_sequence: channelSequence.toString(),\n is_airgap: isAirgap.toString(),\n unified_sbom: unifiedSbom.toString()\n });\n\n const url = `${getApiOrigin()}/enterprise-portal/security-sbom?${params.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching security SBOM via %s\", url);\n }\n\n const response = await authenticatedFetch(url, {\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n // Handle 204 No Content response\n if (response.status === 204) {\n return { sboms: {} };\n }\n\n if (!response.ok) {\n throw new Error(\n `Security SBOM request failed (${response.status} ${response.statusText})`\n );\n }\n\n const envelope = await response.json();\n // Extract from Enterprise Portal API envelope\n return envelope.data as GetSecurityInfoSBOMResult;\n }\n});\n\n// =============================================================================\n// Dashboard Types\n// =============================================================================\n\nexport interface FetchTeamStatsInput {\n token: string;\n}\n\nexport interface TeamUser {\n id: string;\n email: string;\n name?: string;\n createdAt?: string;\n}\n\nexport interface ServiceAccountSummary {\n id: string;\n accountName: string;\n customerId: string;\n isRevoked: boolean;\n createdAt: string;\n}\n\nexport interface FetchTeamStatsResult {\n userCount: number;\n serviceAccountCount: number;\n}\n\nexport interface FetchDashboardInstancesInput {\n token: string;\n}\n\nexport interface FetchDashboardInstancesResult {\n onlineActiveCount: number;\n airgapCount: number;\n onlineUpdates: number;\n airgapUpdates: number;\n // NEW: Additional data from composite endpoint\n license?: {\n type: string;\n expiresAt: string | null;\n isEmbeddedClusterDownloadEnabled: boolean;\n isHelmInstallEnabled: boolean;\n };\n teamStats?: {\n userCount: number;\n serviceAccountCount: number;\n supportBundleCount: number;\n };\n}\n\n// =============================================================================\n// Dashboard Actions\n// =============================================================================\n\n/**\n * NEW: Fetches all dashboard data using the composite Enterprise Portal API endpoint.\n * This replaces multiple v3 calls with a single call that returns:\n * - Instances\n * - Channel releases (for calculating updates)\n * - Notifications\n * - User/app/branding context\n */\nexport const fetchDashboardComposite = defineServerAction<\n FetchDashboardInstancesInput,\n FetchDashboardInstancesResult\n>({\n id: \"dashboard/fetch-composite\",\n description: \"Fetches all dashboard data from the composite Enterprise Portal API endpoint\",\n visibility: \"customer\",\n tags: [\"dashboard\", \"enterprise-portal-api\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Dashboard request requires a session token\");\n }\n\n const origin = getApiOrigin();\n const url = `${origin}/enterprise-portal/dashboard`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching dashboard via %s (Enterprise Portal API)\", url);\n }\n\n const response = await authenticatedFetch(url, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Dashboard request failed (${response.status} ${response.statusText})`\n );\n }\n\n const payload = await response.json();\n\n // Extract data from the Enterprise Portal API envelope\n const data = payload.data;\n const allInstances = data?.instances || [];\n const channelReleases = data?.channelReleases || [];\n const licenseData = data?.license || {};\n const teamStats = data?.teamStats || {};\n\n // Split into online and airgap\n const onlineInstances = allInstances.filter((i: { isAirgap?: boolean }) => !i.isAirgap);\n const airgapInstances = allInstances.filter((i: { isAirgap?: boolean }) => i.isAirgap);\n\n // Filter to active online instances (checked in within 24 hours)\n const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1000;\n const activeOnlineInstances = onlineInstances.filter((instance: { lastCheckin?: string }) => {\n const lastCheckin = instance.lastCheckin\n ? new Date(instance.lastCheckin).getTime()\n : 0;\n return lastCheckin > twentyFourHoursAgo;\n });\n\n const onlineActiveCount = activeOnlineInstances.length;\n const airgapCount = airgapInstances.length;\n\n // Calculate updates for active online instances\n const calculateUpdates = (instances: Array<{ channelId?: string; channelSequence?: number }>) => {\n if (!channelReleases.length) return 0;\n\n let numUpdates = 0;\n for (const instance of instances) {\n const instanceSequence = instance.channelSequence ?? 0;\n const matchingReleases = channelReleases.filter(\n (release: { channelId?: string; channelSequence?: number }) => release.channelId === instance.channelId\n );\n for (const release of matchingReleases) {\n if ((release.channelSequence ?? 0) > instanceSequence) {\n numUpdates++;\n }\n }\n }\n return numUpdates;\n };\n\n const onlineUpdates = calculateUpdates(activeOnlineInstances);\n const airgapUpdates = calculateUpdates(airgapInstances);\n\n // Store the composite data for other dashboard components to use\n // This allows us to eliminate the separate fetchTeamStats, fetchLicenseSummary, and fetchInstallOptions calls\n return {\n onlineActiveCount,\n airgapCount,\n onlineUpdates,\n airgapUpdates,\n // Additional data available from the composite endpoint\n license: licenseData,\n teamStats: teamStats\n };\n }\n});\n\n/**\n * Fetches team statistics including user count and service account count.\n * Used by the Team Settings dashboard card.\n */\nexport const fetchTeamStats = defineServerAction<\n FetchTeamStatsInput,\n FetchTeamStatsResult\n>({\n id: \"dashboard/fetch-team-stats\",\n description: \"Fetches user and service account counts for the dashboard\",\n visibility: \"customer\",\n tags: [\"dashboard\", \"team\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Team stats request requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const origin = getApiOrigin();\n\n // Fetch users count\n let userCount = 0;\n try {\n const usersUrl = `${origin}/v3/users?exclude_invites=false&customer_id=${encodeURIComponent(customerId)}`;\n \n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching team users via %s\", usersUrl);\n }\n\n const usersResponse = await authenticatedFetch(usersUrl, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (usersResponse.ok) {\n const usersData = await usersResponse.json();\n userCount = Array.isArray(usersData.users) ? usersData.users.length : 0;\n }\n } catch (error) {\n console.error(\"[portal-components] Error fetching users:\", error);\n }\n\n // Fetch service accounts count\n let serviceAccountCount = 0;\n try {\n const saUrl = `${origin}/v3/service-accounts?customer_id=${encodeURIComponent(customerId)}`;\n \n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching service accounts via %s\", saUrl);\n }\n\n const saResponse = await authenticatedFetch(saUrl, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (saResponse.ok) {\n const saData = await saResponse.json();\n serviceAccountCount = Array.isArray(saData.serviceAccounts) \n ? saData.serviceAccounts.length \n : 0;\n }\n } catch (error) {\n console.error(\"[portal-components] Error fetching service accounts:\", error);\n }\n\n return {\n userCount,\n serviceAccountCount\n };\n }\n});\n\n/**\n * Fetches instance counts and available updates for the dashboard.\n * Used by the Updates dashboard card.\n */\nexport const fetchDashboardInstances = defineServerAction<\n FetchDashboardInstancesInput,\n FetchDashboardInstancesResult\n>({\n id: \"dashboard/fetch-instances\",\n description: \"Fetches instance counts and update availability for the dashboard\",\n visibility: \"customer\",\n tags: [\"dashboard\", \"instances\", \"updates\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Dashboard instances request requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const origin = getApiOrigin();\n\n // Fetch instances\n const instancesUrl = `${origin}/v3/instances?customer_id=${encodeURIComponent(customerId)}`;\n \n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching instances via %s\", instancesUrl);\n }\n\n const instancesResponse = await authenticatedFetch(instancesUrl, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!instancesResponse.ok) {\n throw new Error(\n `Instances request failed (${instancesResponse.status} ${instancesResponse.statusText})`\n );\n }\n\n const instancesData = await instancesResponse.json();\n const allInstances = instancesData.instances || [];\n\n // Split into online and airgap\n const onlineInstances = allInstances.filter((i: { isAirgap?: boolean }) => !i.isAirgap);\n const airgapInstances = allInstances.filter((i: { isAirgap?: boolean }) => i.isAirgap);\n\n // Filter to active online instances (checked in within 24 hours)\n const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1000;\n const activeOnlineInstances = onlineInstances.filter((instance: { lastCheckin?: string }) => {\n const lastCheckin = instance.lastCheckin \n ? new Date(instance.lastCheckin).getTime() \n : 0;\n return lastCheckin > twentyFourHoursAgo;\n });\n\n const onlineActiveCount = activeOnlineInstances.length;\n const airgapCount = airgapInstances.length;\n\n // Fetch channel releases to calculate updates\n let channelReleases: Array<{ channelId: string; channelSequence: number }> = [];\n try {\n const releasesUrl = `${origin}/v3/channel-releases?customer_id=${encodeURIComponent(customerId)}`;\n \n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching channel releases via %s\", releasesUrl);\n }\n\n const releasesResponse = await authenticatedFetch(releasesUrl, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (releasesResponse.ok) {\n const releasesData = await releasesResponse.json();\n channelReleases = releasesData.channelReleases || [];\n }\n } catch (error) {\n console.error(\"[portal-components] Error fetching channel releases:\", error);\n }\n\n // Calculate updates for active online instances\n const calculateUpdates = (instances: Array<{ channelId?: string; channelSequence?: number }>) => {\n if (!channelReleases.length) return 0;\n \n let numUpdates = 0;\n for (const instance of instances) {\n const instanceSequence = instance.channelSequence ?? 0;\n const matchingReleases = channelReleases.filter(\n (release) => release.channelId === instance.channelId\n );\n for (const release of matchingReleases) {\n if (release.channelSequence > instanceSequence) {\n numUpdates++;\n }\n }\n }\n return numUpdates;\n };\n\n const onlineUpdates = calculateUpdates(activeOnlineInstances);\n const airgapUpdates = calculateUpdates(airgapInstances);\n\n return {\n onlineActiveCount,\n airgapCount,\n onlineUpdates,\n airgapUpdates\n };\n }\n});\n\n// =============================================================================\n// User Settings Types\n// =============================================================================\n\nexport interface FetchCurrentUserInput {\n token: string;\n}\n\nexport interface UserProfile {\n emailAddress: string;\n firstName: string;\n lastName: string;\n}\n\nexport interface FetchCurrentUserResult {\n user: UserProfile;\n}\n\nexport interface UpdateUserInput {\n token: string;\n firstName?: string;\n lastName?: string;\n}\n\nexport interface UpdateUserResult {\n success: boolean;\n}\n\nexport interface NotificationSetting {\n type: string;\n enabled: boolean;\n}\n\nexport interface FetchNotificationsInput {\n token: string;\n customerId: string;\n}\n\nexport interface FetchNotificationsResult {\n notifications: NotificationSetting[];\n}\n\nexport interface UpdateNotificationsInput {\n token: string;\n customerId: string;\n notifications: NotificationSetting[];\n}\n\nexport interface UpdateNotificationsResult {\n notifications: NotificationSetting[];\n}\n\n// =============================================================================\n// User Settings Actions\n// =============================================================================\n\n/**\n * Fetches the current user's profile information.\n */\nexport const fetchCurrentUser = defineServerAction<\n FetchCurrentUserInput,\n FetchCurrentUserResult\n>({\n id: \"user/fetch-current\",\n description: \"Fetches the current user's profile information\",\n visibility: \"customer\",\n tags: [\"user\", \"profile\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Fetch current user requires a session token\");\n }\n\n // NEW: Use Enterprise Portal API endpoint\n const endpoint = `${getApiOrigin()}/enterprise-portal/user/profile`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching current user via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Fetch current user request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n \n return {\n user: {\n emailAddress: data.emailAddress || \"\",\n firstName: data.firstName || \"\",\n lastName: data.lastName || \"\"\n }\n };\n }\n});\n\n/**\n * Updates the current user's profile information.\n */\nexport const updateUser = defineServerAction<\n UpdateUserInput,\n UpdateUserResult\n>({\n id: \"user/update\",\n description: \"Updates the current user's first and/or last name\",\n visibility: \"customer\",\n tags: [\"user\", \"profile\"],\n async run({ token, firstName, lastName }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Update user requires a session token\");\n }\n\n if (!firstName && !lastName) {\n throw new Error(\"At least one of firstName or lastName must be provided\");\n }\n\n // NEW: Use Enterprise Portal API endpoint\n const endpoint = `${getApiOrigin()}/enterprise-portal/user/profile`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] updating user via %s\", endpoint);\n }\n\n const body: { firstName?: string; lastName?: string } = {};\n if (firstName !== undefined) body.firstName = firstName;\n if (lastName !== undefined) body.lastName = lastName;\n\n const response = await authenticatedFetch(endpoint, {\n method: \"PUT\",\n token,\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify(body),\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n throw new Error(\n `Update user request failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n return { success: true };\n }\n});\n\n/**\n * Fetches notification preferences for a specific customer/team.\n */\nexport const fetchNotifications = defineServerAction<\n FetchNotificationsInput,\n FetchNotificationsResult\n>({\n id: \"notifications/fetch\",\n description: \"Fetches notification preferences for a specific team\",\n visibility: \"customer\",\n tags: [\"notifications\", \"user\"],\n async run({ token, customerId }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Fetch notifications requires a session token\");\n }\n\n if (!customerId || typeof customerId !== \"string\") {\n throw new Error(\"Fetch notifications requires a valid customerId\");\n }\n\n // NOTE: customerId is still required in query params because this endpoint\n // delegates to v3 handler which validates customer_id\n const endpoint = `${getApiOrigin()}/enterprise-portal/user/notifications?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching notifications via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Fetch notifications request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n \n return {\n notifications: data.notifications || []\n };\n }\n});\n\n/**\n * Updates notification preferences for a specific customer/team.\n */\nexport const updateNotifications = defineServerAction<\n UpdateNotificationsInput,\n UpdateNotificationsResult\n>({\n id: \"notifications/update\",\n description: \"Updates notification preferences for a specific team\",\n visibility: \"customer\",\n tags: [\"notifications\", \"user\"],\n async run({ token, customerId, notifications }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Update notifications requires a session token\");\n }\n\n if (!customerId || typeof customerId !== \"string\") {\n throw new Error(\"Update notifications requires a valid customerId\");\n }\n\n if (!Array.isArray(notifications)) {\n throw new Error(\"Update notifications requires a notifications array\");\n }\n\n // NOTE: customerId is still required in query params because this endpoint\n // delegates to v3 handler which validates customer_id\n const endpoint = `${getApiOrigin()}/enterprise-portal/user/notifications?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] updating notifications via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"PUT\",\n token,\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify({ notifications }),\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n throw new Error(\n `Update notifications request failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n const data = await response.json();\n \n return {\n notifications: data.notifications || []\n };\n }\n});\n\n// =============================================================================\n// Team Settings Types\n// =============================================================================\n\nexport interface TeamUser {\n id: string;\n emailAddress: string;\n firstAccessedAt?: string;\n lastAccessedAt?: string;\n viewCount?: number;\n pendingInvite: boolean;\n}\n\nexport interface FetchTeamUsersInput {\n token: string;\n limit?: number;\n offset?: number;\n}\n\nexport interface FetchTeamUsersResult {\n users: TeamUser[];\n total: number;\n}\n\nexport interface InviteUserInput {\n token: string;\n email: string;\n}\n\nexport interface InviteUserResult {\n success: boolean;\n}\n\nexport interface DeleteUserInput {\n token: string;\n id: string;\n isPendingInvite: boolean; // If true, delete invite; if false, delete user\n}\n\nexport interface DeleteUserResult {\n success: boolean;\n}\n\nexport interface ServiceAccount {\n id: string;\n customerId: string;\n accountName: string;\n emailAddress?: string;\n token: string;\n isRevoked: boolean;\n createdAt: string;\n lastUsedAt?: string;\n tokenRegeneratedAt?: string;\n}\n\nexport interface FetchServiceAccountsInput {\n token: string;\n limit?: number;\n offset?: number;\n includeRevoked?: boolean;\n}\n\nexport interface FetchServiceAccountsResult {\n serviceAccounts: ServiceAccount[];\n total: number;\n}\n\nexport interface RevokeServiceAccountInput {\n token: string;\n accountId: string;\n}\n\nexport interface RevokeServiceAccountResult {\n success: boolean;\n}\n\nexport interface RotateServiceAccountTokenInput {\n token: string;\n accountId: string;\n}\n\nexport interface RotateServiceAccountTokenResult {\n serviceAccount: ServiceAccount;\n helmLoginCommand: string;\n redeployHelm: string[];\n}\n\nexport interface Instance {\n id: string;\n serviceAccountId?: string;\n versionLabel?: string;\n channelId?: string;\n channelSequence?: number;\n lastCheckin?: string;\n isAirgap?: boolean;\n embeddedClusterVersion?: string;\n tags?: Array<{ key: string; value: string }>;\n}\n\nexport interface FetchInstancesInput {\n token: string;\n}\n\nexport interface FetchInstancesResult {\n instances: Instance[];\n}\n\nexport interface SAMLConfig {\n samlAllowed: boolean;\n samlEnabled: boolean;\n entityId: string;\n acsUrl: string;\n hasIdpMetadata: boolean;\n hasIdpCert: boolean;\n}\n\nexport interface FetchSamlConfigInput {\n token: string;\n}\n\nexport interface FetchSamlConfigResult {\n config: SAMLConfig;\n}\n\nexport interface UpdateSamlConfigInput {\n token: string;\n idpMetadataXml: string;\n idpPublicCert: string;\n}\n\nexport interface UpdateSamlConfigResult {\n success: boolean;\n}\n\nexport interface ToggleSamlEnabledInput {\n token: string;\n enabled: boolean;\n}\n\nexport interface ToggleSamlEnabledResult {\n success: boolean;\n samlEnabled: boolean;\n}\n\nexport interface DeprovisionSamlInput {\n token: string;\n}\n\nexport interface DeprovisionSamlResult {\n success: boolean;\n}\n\n// =============================================================================\n// Team Settings Actions\n// =============================================================================\n\n/**\n * Fetches the list of users for a team.\n */\nexport const fetchTeamUsers = defineServerAction<\n FetchTeamUsersInput,\n FetchTeamUsersResult\n>({\n id: \"team/fetch-users\",\n description: \"Fetches paginated list of team users and pending invites\",\n visibility: \"customer\",\n tags: [\"team\", \"users\"],\n async run({ token, limit = 25, offset = 0 }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Fetch team users requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n // NEW: Use Enterprise Portal API endpoint\n // Note: v3 handler still requires customer_id in query params for validation\n const params = new URLSearchParams({\n customer_id: customerId,\n limit: limit.toString(),\n offset: offset.toString()\n });\n\n const endpoint = `${getApiOrigin()}/enterprise-portal/team/users?${params.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching team users via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Fetch team users request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n \n return {\n users: data.users || [],\n total: data.total || 0\n };\n }\n});\n\n/**\n * Invites a user to the team.\n */\nexport const inviteUser = defineServerAction<\n InviteUserInput,\n InviteUserResult\n>({\n id: \"team/invite-user\",\n description: \"Sends an invitation email to join the team\",\n visibility: \"customer\",\n tags: [\"team\", \"users\", \"invite\"],\n async run({ token, email }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Invite user requires a session token\");\n }\n\n if (!email || typeof email !== \"string\") {\n throw new Error(\"Invite user requires an email address\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n const params = new URLSearchParams({\n email_address: email\n });\n\n const endpoint = `${getApiOrigin()}/enterprise-portal/team/invite?${params.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] inviting user via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"POST\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n let errorMessage = \"Failed to invite user\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n return { success: true };\n }\n});\n\n/**\n * Removes a user or pending invite from the team by ID.\n */\nexport const deleteUser = defineServerAction<\n DeleteUserInput,\n DeleteUserResult\n>({\n id: \"team/delete-user\",\n description: \"Removes a user or pending invite from the team by ID\",\n visibility: \"customer\",\n tags: [\"team\", \"users\", \"delete\"],\n async run({ token, id, isPendingInvite }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Delete user requires a session token\");\n }\n\n if (!id || typeof id !== \"string\") {\n throw new Error(\"Delete user requires an ID\");\n }\n\n // Use different endpoints for users vs invites\n const resource = isPendingInvite ? \"invites\" : \"users\";\n const endpoint = `${getApiOrigin()}/enterprise-portal/team/${resource}/${encodeURIComponent(id)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] deleting %s %s via %s\", resource, id, endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"DELETE\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n let errorMessage = isPendingInvite ? \"Failed to delete invite\" : \"Failed to delete user\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n return { success: true };\n }\n});\n\n/**\n * Fetches the list of service accounts for a team.\n */\nexport const fetchServiceAccounts = defineServerAction<\n FetchServiceAccountsInput,\n FetchServiceAccountsResult\n>({\n id: \"team/fetch-service-accounts\",\n description: \"Fetches paginated list of service accounts\",\n visibility: \"customer\",\n tags: [\"team\", \"service-accounts\"],\n async run({ token, limit = 50, offset = 0, includeRevoked = false }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Fetch service accounts requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const params = new URLSearchParams({\n customer_id: customerId,\n limit: limit.toString(),\n offset: offset.toString()\n });\n\n // Add filterRevoked parameter - API filters revoked when this param is present\n if (!includeRevoked) {\n params.set(\"filterRevoked\", \"false\");\n }\n\n const endpoint = `${getApiOrigin()}/enterprise-portal/team/service-accounts?${params.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching service accounts via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Fetch service accounts request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n \n return {\n serviceAccounts: data.serviceAccounts || [],\n total: data.total || 0\n };\n }\n});\n\n/**\n * Revokes a service account.\n */\nexport const revokeServiceAccount = defineServerAction<\n RevokeServiceAccountInput,\n RevokeServiceAccountResult\n>({\n id: \"team/revoke-service-account\",\n description: \"Revokes a service account (soft delete)\",\n visibility: \"customer\",\n tags: [\"team\", \"service-accounts\", \"revoke\"],\n async run({ token, accountId }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Revoke service account requires a session token\");\n }\n\n if (!accountId || typeof accountId !== \"string\") {\n throw new Error(\"Revoke service account requires an account ID\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/enterprise-portal/team/service-accounts/${encodeURIComponent(accountId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] revoking service account via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"DELETE\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n let errorMessage = \"Failed to revoke service account\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n return { success: true };\n }\n});\n\n/**\n * Rotates a service account token.\n */\nexport const rotateServiceAccountToken = defineServerAction<\n RotateServiceAccountTokenInput,\n RotateServiceAccountTokenResult\n>({\n id: \"team/rotate-service-account-token\",\n description: \"Generates a new token for a service account\",\n visibility: \"customer\",\n tags: [\"team\", \"service-accounts\", \"rotate\"],\n async run({ token, accountId }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Rotate service account token requires a session token\");\n }\n\n if (!accountId || typeof accountId !== \"string\") {\n throw new Error(\"Rotate service account token requires an account ID\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/enterprise-portal/team/service-accounts/${encodeURIComponent(accountId)}/rotate-token`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] rotating service account token via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"POST\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n let errorMessage = \"Failed to rotate service account token\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n const data = await response.json();\n \n return {\n serviceAccount: data.service_account,\n helmLoginCommand: data.helm_login_cmd || \"\",\n redeployHelm: data.redeploy_helm || []\n };\n }\n});\n\n/**\n * Fetches instances for the customer.\n */\nexport const fetchInstances = defineServerAction<\n FetchInstancesInput,\n FetchInstancesResult\n>({\n id: \"team/fetch-instances\",\n description: \"Fetches instances to determine service account usage\",\n visibility: \"customer\",\n tags: [\"team\", \"instances\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Fetch instances requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/v3/instances?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching instances via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Fetch instances request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n \n return {\n instances: data.instances || []\n };\n }\n});\n\n/**\n * Fetches SAML configuration for the customer.\n */\nexport const fetchSamlConfig = defineServerAction<\n FetchSamlConfigInput,\n FetchSamlConfigResult\n>({\n id: \"team/fetch-saml-config\",\n description: \"Fetches SAML SSO configuration for the team\",\n visibility: \"customer\",\n tags: [\"team\", \"saml\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Fetch SAML config requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/v3/customer/saml/config?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching SAML config via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Fetch SAML config request failed (${response.status} ${response.statusText})`\n );\n }\n\n const data = await response.json();\n \n return {\n config: {\n samlAllowed: data.samlAllowed || false,\n samlEnabled: data.samlEnabled || false,\n entityId: data.entityId || \"\",\n acsUrl: data.acsUrl || \"\",\n hasIdpMetadata: data.hasIdpMetadata || false,\n hasIdpCert: data.hasIdpCert || false\n }\n };\n }\n});\n\n/**\n * Updates SAML configuration (uploads IdP metadata and certificate).\n */\nexport const updateSamlConfig = defineServerAction<\n UpdateSamlConfigInput,\n UpdateSamlConfigResult\n>({\n id: \"team/update-saml-config\",\n description: \"Uploads IdP metadata and certificate for SAML SSO\",\n visibility: \"customer\",\n tags: [\"team\", \"saml\", \"update\"],\n async run({ token, idpMetadataXml, idpPublicCert }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Update SAML config requires a session token\");\n }\n\n if (!idpMetadataXml || !idpPublicCert) {\n throw new Error(\"Both IdP metadata and certificate are required\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/v3/customer/saml/config?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] updating SAML config via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"PUT\",\n token,\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify({\n idpMetadataXml,\n idpPublicCert\n }),\n signal: context?.signal\n });\n\n if (!response.ok) {\n let errorMessage = \"Failed to update SAML configuration\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n return { success: true };\n }\n});\n\n/**\n * Toggles SAML authentication enabled/disabled.\n */\nexport const toggleSamlEnabled = defineServerAction<\n ToggleSamlEnabledInput,\n ToggleSamlEnabledResult\n>({\n id: \"team/toggle-saml-enabled\",\n description: \"Enables or disables SAML authentication\",\n visibility: \"customer\",\n tags: [\"team\", \"saml\", \"toggle\"],\n async run({ token, enabled }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Toggle SAML enabled requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/v3/customer/saml/enable?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] toggling SAML enabled via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"PUT\",\n token,\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify({ enabled }),\n signal: context?.signal\n });\n\n if (!response.ok) {\n let errorMessage = \"Failed to toggle SAML\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n const data = await response.json();\n \n return {\n success: true,\n samlEnabled: data.samlEnabled || enabled\n };\n }\n});\n\n/**\n * Removes SAML configuration (deprovisions SAML).\n */\nexport const deprovisionSaml = defineServerAction<\n DeprovisionSamlInput,\n DeprovisionSamlResult\n>({\n id: \"team/deprovision-saml\",\n description: \"Removes all SAML configuration\",\n visibility: \"customer\",\n tags: [\"team\", \"saml\", \"delete\"],\n async run({ token }, context) {\n if (!token || typeof token !== \"string\") {\n throw new Error(\"Deprovision SAML requires a session token\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const endpoint = `${getApiOrigin()}/v3/customer/saml/config?customer_id=${encodeURIComponent(customerId)}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] deprovisioning SAML via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"DELETE\",\n token,\n headers: { accept: \"application/json\" },\n signal: context?.signal\n });\n\n if (!response.ok) {\n let errorMessage = \"Failed to remove SAML configuration\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n return { success: true };\n }\n});\n\n// =============================================================================\n// Invite Accept/Refresh Actions\n// =============================================================================\n\nexport interface AcceptInviteInput {\n code: string;\n}\n\nexport interface AcceptInviteResult {\n token: string;\n}\n\nexport interface AcceptInviteError {\n code: \"invalid_code\" | \"expired\" | \"unknown\";\n message: string;\n}\n\n/**\n * Accepts a team invitation using the invite code from email.\n * Returns a JWT token on success that can be used to establish a session.\n */\nexport const acceptInvite = defineServerAction<\n AcceptInviteInput,\n AcceptInviteResult\n>({\n id: \"auth/accept-invite\",\n description: \"Accepts a team invitation and returns a session token\",\n visibility: \"customer\",\n tags: [\"auth\", \"invite\", \"join\"],\n async run({ code }) {\n if (!code || typeof code !== \"string\") {\n const error: AcceptInviteError = {\n code: \"invalid_code\",\n message: \"Invite code is required\"\n };\n throw error;\n }\n\n // NEW: Use Enterprise Portal API auth endpoint\n const endpoint = `${getApiOrigin()}/enterprise-portal/auth/invite/accept`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] accepting invite via %s (Enterprise Portal API)\", endpoint);\n }\n\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify({ code })\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n const error: AcceptInviteError = {\n code: \"invalid_code\",\n message: \"Invalid or expired invite code. Please check your code and try again.\"\n };\n throw error;\n }\n\n let errorMessage = \"Failed to accept invitation\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n\n const error: AcceptInviteError = {\n code: \"unknown\",\n message: errorMessage\n };\n throw error;\n }\n\n const payload = await response.json();\n const token = payload?.jwt ?? payload?.token;\n\n if (typeof token !== \"string\") {\n throw new Error(\"Invite accepted but no token returned\");\n }\n\n return { token };\n }\n});\n\nexport interface RefreshInviteInput {\n code: string;\n}\n\nexport interface RefreshInviteResult {\n success: boolean;\n}\n\n/**\n * Refreshes an expired invite by generating a new code and resending the email.\n * The original code is used to identify the invite to refresh.\n */\nexport const refreshInvite = defineServerAction<\n RefreshInviteInput,\n RefreshInviteResult\n>({\n id: \"auth/refresh-invite\",\n description: \"Refreshes an expired invite and resends the invitation email\",\n visibility: \"customer\",\n tags: [\"auth\", \"invite\", \"refresh\"],\n async run({ code }) {\n if (!code || typeof code !== \"string\") {\n throw new Error(\"Invite code is required\");\n }\n\n // NEW: Use Enterprise Portal API auth endpoint\n const endpoint = `${getApiOrigin()}/enterprise-portal/auth/invite/refresh`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] refreshing invite via %s (Enterprise Portal API)\", endpoint);\n }\n\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify({ code })\n });\n\n // The API returns 200 even for non-existent codes to prevent code enumeration\n if (!response.ok) {\n let errorMessage = \"Failed to refresh invitation\";\n try {\n const data = await response.json();\n errorMessage = data.message || data.error || errorMessage;\n } catch {\n // Ignore JSON parse errors\n }\n throw new Error(errorMessage);\n }\n\n return { success: true };\n }\n});\n","/**\n * Install-related server actions for the Linux and Helm installation wizards.\n * \n * These actions handle the complete installation flow including:\n * - Fetching available releases (channel releases)\n * - Creating/updating/fetching install options\n * - Generating installation instructions\n */\n\nimport { authenticatedFetch } from \"../utils/api-client\";\nimport { getApiOrigin, getCustomerIdFromToken } from \"./index\";\nimport type { PortalActionContext } from \"./index\";\n\n// =============================================================================\n// Types - Channel Releases\n// =============================================================================\n\nexport interface EmbeddedClusterInstallationType {\n version?: string;\n}\n\nexport interface HelmInstallationType {\n version?: string;\n}\n\nexport interface InstallationTypes {\n embeddedCluster?: EmbeddedClusterInstallationType;\n helm?: HelmInstallationType;\n}\n\nexport interface ChannelRelease {\n channelId: string;\n channelName: string;\n channelSlug: string;\n channelSequence: number;\n releaseSequence: number;\n versionLabel: string;\n releaseNotes?: string;\n createdAt: string;\n isRequired?: boolean;\n installationTypes?: InstallationTypes;\n}\n\nexport interface FetchChannelReleasesInput {\n token: string;\n channelId?: string;\n}\n\nexport interface FetchChannelReleasesResult {\n channelReleases: ChannelRelease[];\n}\n\n// =============================================================================\n// Types - Install Options\n// =============================================================================\n\nexport type NetworkAvailability = \"online\" | \"proxy\" | \"airgap\";\nexport type InstallType = \"linux\" | \"helm\";\nexport type RegistryAvailability = \"online\" | \"partial\" | \"offline\";\nexport type KubernetesDistribution = \"vanilla\" | \"openshift\" | \"rancher\" | \"aks\" | \"eks\" | \"gke\";\nexport type InstallStatus = \"in_progress\" | \"completed\" | \"discarded\";\n\nexport interface InstallStep {\n step_number: number;\n step_name: string;\n title: string;\n description?: string;\n commands: string[];\n /** If true, this step can be marked as completed (shows checkmark UI) */\n maybe_completed?: boolean;\n}\n\nexport interface InstallInstructions {\n format: \"basic\" | \"mdx\";\n install_type: string;\n title: string;\n steps?: InstallStep[];\n mdx_template?: string;\n context?: Record<string, unknown>;\n renderer_version?: number;\n}\n\nexport interface InstallOptions {\n id: string;\n customer_id: string;\n install_type: InstallType;\n instance_name: string;\n instance_id?: string;\n service_account_id?: string;\n service_account_email_address?: string;\n license_id?: string;\n started_at: string;\n completed_at?: string;\n network_availability?: NetworkAvailability;\n is_multi_node?: boolean;\n channel_id?: string;\n channel_release_sequence?: number;\n registry_availability?: RegistryAvailability;\n kubernetes_distribution?: KubernetesDistribution;\n admin_console_url?: string;\n status?: InstallStatus;\n}\n\nexport interface CreateInstallOptionsInput {\n token: string;\n installType: InstallType;\n instanceName: string;\n serviceAccountId: string;\n networkAvailability: NetworkAvailability;\n isMultiNode?: boolean;\n channelId?: string;\n channelReleaseSequence?: number;\n // Helm-specific\n registryAvailability?: RegistryAvailability;\n kubernetesDistribution?: KubernetesDistribution;\n}\n\nexport interface CreateInstallOptionsResult {\n install_options: InstallOptions;\n instructions?: InstallInstructions;\n}\n\nexport interface GetInstallOptionsInput {\n token: string;\n installOptionsId: string;\n includeInstructions?: boolean;\n privateRegistryHostname?: string;\n proxyUrl?: string;\n}\n\nexport interface GetInstallOptionsResult {\n id: string;\n customer_id: string;\n install_type: InstallType;\n instance_name: string;\n instance_id?: string;\n service_account_id?: string;\n service_account_email_address?: string;\n started_at: string;\n completed_at?: string;\n network_availability?: NetworkAvailability;\n is_multi_node?: boolean;\n channel_id?: string;\n channel_release_sequence?: number;\n registry_availability?: RegistryAvailability;\n kubernetes_distribution?: KubernetesDistribution;\n admin_console_url?: string;\n status?: InstallStatus;\n instructions?: InstallInstructions;\n /** Timestamp when assets were downloaded (for progress tracking) */\n assets_downloaded_at?: string;\n /** Timestamp when registry authentication was completed (for Helm progress tracking) */\n registry_authenticated_at?: string;\n /** Timestamp when images were pulled (for Helm progress tracking) */\n images_pulled_at?: string;\n /** Timestamp when installation was completed (for progress tracking) */\n installation_completed_at?: string;\n}\n\nexport interface UpdateInstallOptionsInput {\n token: string;\n installOptionsId: string;\n installType?: InstallType;\n channelId?: string;\n channelReleaseSequence?: number;\n networkAvailability?: NetworkAvailability;\n registryAvailability?: RegistryAvailability;\n kubernetesDistribution?: KubernetesDistribution;\n isMultiNode?: boolean;\n serviceAccountId?: string;\n adminConsoleUrl?: string | null;\n status?: InstallStatus;\n includeInstructions?: boolean;\n privateRegistryHostname?: string;\n proxyUrl?: string;\n}\n\nexport interface UpdateInstallOptionsResult {\n install_options: InstallOptions;\n instructions?: InstallInstructions;\n}\n\n// =============================================================================\n// Actions - Channel Releases\n// =============================================================================\n\n/**\n * Fetches available channel releases for the customer.\n * These are filtered to show only releases that have embedded cluster installers.\n */\nexport async function fetchChannelReleases(\n input: FetchChannelReleasesInput,\n context?: PortalActionContext\n): Promise<FetchChannelReleasesResult> {\n const { token, channelId } = input;\n\n if (!token || typeof token !== \"string\") {\n throw new Error(\"fetchChannelReleases requires a session token\");\n }\n\n const origin = getApiOrigin();\n\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n const url = new URL(`${origin}/enterprise-portal/channel-releases`);\n if (channelId) {\n url.searchParams.set(\"channel_id\", channelId);\n }\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching channel releases via %s (Enterprise Portal API)\", url.toString());\n }\n\n const response = await authenticatedFetch(url.toString(), {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Channel releases request failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n const envelope = await response.json();\n const payload = envelope.data;\n\n return {\n channelReleases: payload?.channelReleases || []\n };\n}\n\n// =============================================================================\n// Actions - Install Options\n// =============================================================================\n\n/**\n * Creates a new install options record to track an installation attempt.\n * This is called after creating a service account and before showing install commands.\n */\nexport async function createInstallOptions(\n input: CreateInstallOptionsInput,\n context?: PortalActionContext\n): Promise<CreateInstallOptionsResult> {\n const {\n token,\n installType,\n instanceName,\n serviceAccountId,\n networkAvailability,\n isMultiNode = false,\n channelId,\n channelReleaseSequence,\n registryAvailability,\n kubernetesDistribution\n } = input;\n\n if (!token || typeof token !== \"string\") {\n throw new Error(\"createInstallOptions requires a session token\");\n }\n\n if (!instanceName?.trim()) {\n throw new Error(\"Instance name is required\");\n }\n\n if (!serviceAccountId?.trim()) {\n throw new Error(\"Service account ID is required\");\n }\n\n const origin = getApiOrigin();\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n const endpoint = `${origin}/enterprise-portal/install-options?includeInstructions=true`;\n\n const body: Record<string, unknown> = {\n install_type: installType,\n instance_name: instanceName.trim(),\n service_account_id: serviceAccountId.trim(),\n network_availability: networkAvailability,\n is_multi_node: isMultiNode\n };\n\n // Add optional fields\n if (channelId) {\n body.channel_id = channelId;\n }\n if (channelReleaseSequence !== undefined) {\n body.channel_release_sequence = channelReleaseSequence;\n }\n if (registryAvailability) {\n body.registry_availability = registryAvailability;\n }\n if (kubernetesDistribution) {\n body.kubernetes_distribution = kubernetesDistribution;\n }\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] creating install options via %s\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"POST\",\n token,\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify(body),\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Create install options failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n return await response.json();\n}\n\n/**\n * Fetches existing install options by ID.\n * Can optionally include generated installation instructions.\n */\nexport async function getInstallOptions(\n input: GetInstallOptionsInput,\n context?: PortalActionContext\n): Promise<GetInstallOptionsResult> {\n const {\n token,\n installOptionsId,\n includeInstructions = true,\n privateRegistryHostname,\n proxyUrl\n } = input;\n\n if (!token || typeof token !== \"string\") {\n throw new Error(\"getInstallOptions requires a session token\");\n }\n\n if (!installOptionsId?.trim()) {\n throw new Error(\"Install options ID is required\");\n }\n\n const origin = getApiOrigin();\n\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n const url = new URL(`${origin}/enterprise-portal/install-options/${installOptionsId.trim()}`);\n\n if (includeInstructions) {\n url.searchParams.set(\"includeInstructions\", \"true\");\n }\n if (privateRegistryHostname) {\n url.searchParams.set(\"privateRegistryHostname\", privateRegistryHostname);\n }\n if (proxyUrl) {\n url.searchParams.set(\"proxyUrl\", proxyUrl);\n }\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching install options via %s (Enterprise Portal API)\", url.toString());\n }\n\n const response = await authenticatedFetch(url.toString(), {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Get install options failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n const envelope = await response.json();\n return envelope.data;\n}\n\n/**\n * Updates existing install options.\n * Typically called when user selects a different version or changes settings.\n * Returns updated options with regenerated installation instructions.\n */\nexport async function updateInstallOptions(\n input: UpdateInstallOptionsInput,\n context?: PortalActionContext\n): Promise<UpdateInstallOptionsResult> {\n const {\n token,\n installOptionsId,\n installType,\n channelId,\n channelReleaseSequence,\n networkAvailability,\n registryAvailability,\n kubernetesDistribution,\n isMultiNode,\n serviceAccountId,\n adminConsoleUrl,\n status,\n includeInstructions = true,\n privateRegistryHostname,\n proxyUrl\n } = input;\n\n if (!token || typeof token !== \"string\") {\n throw new Error(\"updateInstallOptions requires a session token\");\n }\n\n if (!installOptionsId?.trim()) {\n throw new Error(\"Install options ID is required\");\n }\n\n const origin = getApiOrigin();\n\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n const url = new URL(`${origin}/enterprise-portal/install-options/${installOptionsId.trim()}`);\n\n if (includeInstructions) {\n url.searchParams.set(\"includeInstructions\", \"true\");\n }\n if (privateRegistryHostname) {\n url.searchParams.set(\"privateRegistryHostname\", privateRegistryHostname);\n }\n if (proxyUrl) {\n url.searchParams.set(\"proxyUrl\", proxyUrl);\n }\n\n // Build body with only provided fields\n const body: Record<string, unknown> = {};\n\n if (installType !== undefined) {\n body.install_type = installType;\n }\n if (channelId !== undefined) {\n body.channel_id = channelId;\n }\n if (channelReleaseSequence !== undefined) {\n body.channel_release_sequence = channelReleaseSequence;\n }\n if (networkAvailability !== undefined) {\n body.network_availability = networkAvailability;\n }\n if (registryAvailability !== undefined) {\n body.registry_availability = registryAvailability;\n }\n if (kubernetesDistribution !== undefined) {\n body.kubernetes_distribution = kubernetesDistribution;\n }\n if (isMultiNode !== undefined) {\n body.is_multi_node = isMultiNode;\n }\n if (serviceAccountId !== undefined) {\n body.service_account_id = serviceAccountId;\n }\n if (adminConsoleUrl !== undefined) {\n body.admin_console_url = adminConsoleUrl;\n }\n if (status !== undefined) {\n body.status = status;\n }\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] updating install options via %s\", url.toString());\n }\n\n const response = await authenticatedFetch(url.toString(), {\n method: \"PATCH\",\n token,\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\"\n },\n body: JSON.stringify(body),\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Update install options failed (${response.status} ${response.statusText}): ${errorText}`\n );\n }\n\n return await response.json();\n}\n\n// =============================================================================\n// Helper - Filter Releases with Embedded Cluster Support\n// =============================================================================\n\n/**\n * Filters channel releases to only include those with embedded cluster installers.\n * Use this when populating the version dropdown for Linux installs.\n */\nexport function filterEmbeddedClusterReleases(releases: ChannelRelease[]): ChannelRelease[] {\n return releases.filter(\n (release) => release.installationTypes?.embeddedCluster?.version\n );\n}\n\n/**\n * Filters channel releases to only include those with Helm chart support.\n * Use this when populating the version dropdown for Helm installs.\n */\nexport function filterHelmReleases(releases: ChannelRelease[]): ChannelRelease[] {\n return releases.filter(\n (release) => release.installationTypes?.helm?.version\n );\n}\n\n// =============================================================================\n// Types - Update Instructions\n// =============================================================================\n\nexport interface GetUpdateInstructionsInput {\n token: string;\n /** Install options ID (if known) */\n installOptionsId?: string;\n /** Instance ID (used if installOptionsId is not available) */\n instanceId?: string;\n /** Target channel ID for the update */\n targetChannelId: string;\n /** Target channel sequence for the update */\n targetChannelSequence: number;\n /** Private registry hostname (for airgap with private registry) */\n privateRegistryHostname?: string;\n}\n\nexport interface UpdateInstructions {\n format: string;\n install_type: string;\n title: string;\n steps: InstallStep[];\n}\n\nexport interface GetUpdateInstructionsResult {\n instructions: UpdateInstructions;\n}\n\n// =============================================================================\n// Actions - Update Instructions\n// =============================================================================\n\n/**\n * Fetches update instructions for an instance.\n * Returns step-by-step commands for updating to a target version.\n */\nexport async function getUpdateInstructions(\n input: GetUpdateInstructionsInput,\n context?: PortalActionContext\n): Promise<GetUpdateInstructionsResult> {\n const {\n token,\n installOptionsId,\n instanceId,\n targetChannelId,\n targetChannelSequence,\n privateRegistryHostname\n } = input;\n\n if (!token || typeof token !== \"string\") {\n throw new Error(\"getUpdateInstructions requires a session token\");\n }\n\n if (!targetChannelId?.trim()) {\n throw new Error(\"Target channel ID is required\");\n }\n\n if (targetChannelSequence === undefined || targetChannelSequence === null) {\n throw new Error(\"Target channel sequence is required\");\n }\n\n // Need either installOptionsId or instanceId\n const identifier = installOptionsId || instanceId;\n if (!identifier) {\n throw new Error(\"Either installOptionsId or instanceId is required\");\n }\n\n const customerId = getCustomerIdFromToken(token);\n const origin = getApiOrigin();\n\n // Build query params\n const queryParams = new URLSearchParams();\n queryParams.set(\"targetChannelId\", targetChannelId.trim());\n queryParams.set(\"targetChannelSequence\", targetChannelSequence.toString());\n \n if (privateRegistryHostname?.trim()) {\n queryParams.set(\"privateRegistryHostname\", privateRegistryHostname.trim());\n }\n \n // When using instance_id, we still need a path parameter for the route\n // but it will be ignored by the backend in favor of the instance_id query param\n if (!installOptionsId && instanceId) {\n queryParams.set(\"instance_id\", instanceId);\n }\n\n const pathId = installOptionsId || \"placeholder\";\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n const url = `${origin}/enterprise-portal/install-options/${pathId}/update-instructions?${queryParams.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching update instructions via %s\", url);\n }\n\n const response = await authenticatedFetch(url, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n let errorMessage = \"Failed to fetch update instructions\";\n try {\n const errorData = JSON.parse(errorText);\n errorMessage = errorData.error || errorData.message || errorMessage;\n } catch {\n // Use default message\n }\n throw new Error(errorMessage);\n }\n\n const data = await response.json();\n\n return {\n instructions: {\n format: data.format || \"steps\",\n install_type: data.install_type,\n title: data.title,\n steps: data.steps || []\n }\n };\n}\n\n// =============================================================================\n// Types - Pending Installations\n// =============================================================================\n\nexport interface FetchPendingInstallationsInput {\n token: string;\n}\n\nexport interface PendingInstallation {\n id: string;\n name: string;\n method: \"helm\" | \"linux\";\n startedBy: string;\n startedAt: string;\n}\n\nexport interface FetchPendingInstallationsResult {\n installations: PendingInstallation[];\n}\n\n// =============================================================================\n// Fetch Pending Installations\n// =============================================================================\n\n/**\n * Fetches pending (in-progress) installations for the current user.\n * Returns up to 5 most recent installations, ordered by creation date descending.\n */\nexport async function fetchPendingInstallations(\n input: FetchPendingInstallationsInput,\n context?: PortalActionContext\n): Promise<FetchPendingInstallationsResult> {\n const { token } = input;\n \n if (typeof token !== \"string\" || token.trim().length === 0) {\n throw new Error(\"fetchPendingInstallations requires a non-empty token\");\n }\n\n const origin = getApiOrigin();\n\n // NEW: Use Enterprise Portal API endpoint (no customer_id needed)\n // This endpoint returns 200 with empty array instead of 404 when no results\n const queryParams = new URLSearchParams();\n queryParams.set(\"status\", \"in_progress\");\n queryParams.set(\"page_size\", \"5\");\n queryParams.set(\"order_by\", \"created_at\");\n queryParams.set(\"order_direction\", \"desc\");\n\n const endpoint = `${origin}/enterprise-portal/install-options?${queryParams.toString()}`;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.debug(\"[portal-components] fetching pending installations via %s (Enterprise Portal API)\", endpoint);\n }\n\n const response = await authenticatedFetch(endpoint, {\n method: \"GET\",\n token,\n headers: {\n accept: \"application/json\"\n },\n signal: context?.signal\n });\n\n if (!response.ok) {\n throw new Error(\n `Pending installations request failed (${response.status} ${response.statusText})`\n );\n }\n\n const envelope = await response.json();\n const payload = envelope.data;\n\n // Parse the response - expecting {install_options: [...]}\n const installArray = payload?.install_options || [];\n\n const installations: PendingInstallation[] = installArray.map((item: Record<string, unknown>) => ({\n id: String(item.id || \"\"),\n name: String(item.instance_name || \"Unknown\"),\n method: item.install_type === \"helm\" ? \"helm\" as const : \"linux\" as const,\n startedBy: String(item.service_account_email_address || \"Unknown\"),\n startedAt: String(item.started_at || new Date().toISOString())\n }));\n\n return {\n installations\n };\n}\n\n"]}