@burdenoff/microfe-auth 2026.608.1 → 2026.624.2

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.
@@ -60,7 +60,7 @@ function f({ onSwitchWorkspace: f, onLogout: p, position: m = "right" }) {
60
60
  className: "px-4 py-3 border-b border-border-subtle",
61
61
  children: [
62
62
  /* @__PURE__ */ u("p", {
63
- className: "text-sm font-medium text-foreground truncate",
63
+ className: "text-sm font-medium text-text-primary truncate",
64
64
  children: E
65
65
  }),
66
66
  /* @__PURE__ */ u("p", {
@@ -1 +1 @@
1
- {"version":3,"file":"UserProfileMenu.js","names":[],"sources":["../../../../src/auth/components/profile/UserProfileMenu.tsx"],"sourcesContent":["import { useState, useRef, useEffect } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { LogOut, ChevronDown, Building2, UserPlus } from \"lucide-react\";\nimport { useNavigate } from \"react-router-dom\";\nimport { useAuthStore } from \"../../store/authStore\";\n\nexport interface UserProfileMenuProps {\n /**\n * Callback when workspace switcher should be shown\n */\n onSwitchWorkspace?: () => void;\n\n /**\n * Logout handler (REQUIRED)\n * Should clear auth state and redirect to login\n */\n onLogout: () => void;\n\n /**\n * Position of the dropdown menu\n * @default 'right'\n */\n position?: \"left\" | \"right\";\n}\n\n/**\n * User Profile Menu Component\n *\n * Displays user avatar/icon with dropdown menu containing:\n * - User information\n * - Switch Workspace option\n * - Logout button\n *\n * @example\n * ```tsx\n * import { UserProfileMenu } from '@burdenoff/microfe-auth';\n *\n * function AppHeader() {\n * return (\n * <div className=\"header\">\n * <UserProfileMenu\n * onSwitchWorkspace={() => setShowWorkspaceSwitcher(true)}\n * position=\"right\"\n * />\n * </div>\n * );\n * }\n * ```\n */\nexport function UserProfileMenu({\n onSwitchWorkspace,\n onLogout,\n position = \"right\",\n}: UserProfileMenuProps) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const [isOpen, setIsOpen] = useState(false);\n const menuRef = useRef<HTMLDivElement>(null);\n const navigate = useNavigate();\n\n const user = useAuthStore((state) => state.user);\n const currentWorkspaceId = useAuthStore((state) => state.currentWorkspaceId);\n\n // Close dropdown when clicking outside\n useEffect(() => {\n function handleClickOutside(event: MouseEvent) {\n if (menuRef.current && !menuRef.current.contains(event.target as Node)) {\n setIsOpen(false);\n }\n }\n\n if (isOpen) {\n document.addEventListener(\"mousedown\", handleClickOutside);\n return () =>\n document.removeEventListener(\"mousedown\", handleClickOutside);\n }\n }, [isOpen]);\n\n const handleLogout = () => {\n setIsOpen(false);\n onLogout();\n };\n\n const handleSwitchWorkspace = () => {\n setIsOpen(false);\n onSwitchWorkspace?.();\n };\n\n if (!user) {\n return null;\n }\n\n // Get user initials for avatar\n const getInitials = () => {\n if (user.firstName && user.lastName) {\n return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase();\n }\n if (user.name) {\n const parts = user.name.split(\" \");\n if (parts.length >= 2) {\n return `${parts[0][0]}${parts[1][0]}`.toUpperCase();\n }\n return user.name.substring(0, 2).toUpperCase();\n }\n if (user.username) {\n return user.username.substring(0, 2).toUpperCase();\n }\n return user.email.substring(0, 2).toUpperCase();\n };\n\n const displayName =\n user.name ||\n `${user.firstName || \"\"} ${user.lastName || \"\"}`.trim() ||\n user.username;\n\n return (\n <div className=\"relative\" ref={menuRef}>\n {/* Profile Button */}\n <button\n type=\"button\"\n onClick={() => setIsOpen(!isOpen)}\n className=\"flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-bg-sunken transition-colors\"\n aria-label=\"User menu\"\n aria-expanded={isOpen}\n >\n {/* Avatar */}\n <div className=\"size-8 rounded-full bg-accent-blue flex items-center justify-center text-white text-sm font-medium overflow-hidden\">\n {user.avatar ? (\n <img\n src={user.avatar}\n alt={tr(\"auth.profileMenu.avatarAlt\", \"Profile picture\")}\n className=\"size-full object-cover\"\n />\n ) : (\n getInitials()\n )}\n </div>\n\n {/* User Name (optional, hide on small screens) */}\n <span className=\"hidden sm:block text-sm font-medium text-text-secondary\">\n {displayName}\n </span>\n\n {/* Chevron */}\n <ChevronDown\n className={`size-4 text-text-secondary transition-transform ${\n isOpen ? \"rotate-180\" : \"\"\n }`}\n />\n </button>\n\n {/* Dropdown Menu */}\n {isOpen && (\n <div\n className={`absolute top-full mt-2 w-64 bg-bg-surface rounded-lg shadow-lg border border-border-subtle py-2 z-50 ${\n position === \"right\" ? \"right-0\" : \"left-0\"\n }`}\n >\n {/* User Info Section */}\n <div className=\"px-4 py-3 border-b border-border-subtle\">\n <p className=\"text-sm font-medium text-foreground truncate\">\n {displayName}\n </p>\n <p className=\"text-xs text-text-secondary truncate\">{user.email}</p>\n {currentWorkspaceId && (\n <p className=\"text-xs text-text-tertiary mt-1 truncate\">\n Workspace: {currentWorkspaceId.substring(0, 8)}...\n </p>\n )}\n </div>\n\n {/* Menu Items */}\n <div className=\"py-1\">\n {/* Switch Workspace */}\n {onSwitchWorkspace && (\n <button\n type=\"button\"\n onClick={handleSwitchWorkspace}\n className=\"w-full flex items-center gap-3 px-4 py-2 text-sm text-text-secondary hover:bg-bg-sunken transition-colors\"\n >\n <Building2 className=\"size-4\" />\n <span>\n {tr(\"auth.profileMenu.switchWorkspace\", \"Switch Workspace\")}\n </span>\n </button>\n )}\n\n {/* Add Account */}\n <button\n type=\"button\"\n onClick={() => {\n setIsOpen(false);\n const returnTo = encodeURIComponent(\n window.location.pathname + window.location.search,\n );\n navigate(`/auth/login?mode=add-account&returnTo=${returnTo}`);\n }}\n className=\"w-full flex items-center gap-3 px-4 py-2 text-sm text-text-secondary hover:bg-bg-sunken transition-colors\"\n >\n <UserPlus className=\"size-4\" />\n <span>{tr(\"auth.profileMenu.addAccount\", \"Add account\")}</span>\n </button>\n\n {/* Logout */}\n <button\n type=\"button\"\n onClick={handleLogout}\n className=\"w-full flex items-center gap-3 px-4 py-2 text-sm text-status-error-text hover:bg-status-error-bg-subtle transition-colors\"\n >\n <LogOut className=\"size-4\" />\n <span>{tr(\"auth.profileMenu.logout\", \"Logout\")}</span>\n </button>\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"UserProfileMenu.js","names":[],"sources":["../../../../src/auth/components/profile/UserProfileMenu.tsx"],"sourcesContent":["import { useState, useRef, useEffect } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { LogOut, ChevronDown, Building2, UserPlus } from \"lucide-react\";\nimport { useNavigate } from \"react-router-dom\";\nimport { useAuthStore } from \"../../store/authStore\";\n\nexport interface UserProfileMenuProps {\n /**\n * Callback when workspace switcher should be shown\n */\n onSwitchWorkspace?: () => void;\n\n /**\n * Logout handler (REQUIRED)\n * Should clear auth state and redirect to login\n */\n onLogout: () => void;\n\n /**\n * Position of the dropdown menu\n * @default 'right'\n */\n position?: \"left\" | \"right\";\n}\n\n/**\n * User Profile Menu Component\n *\n * Displays user avatar/icon with dropdown menu containing:\n * - User information\n * - Switch Workspace option\n * - Logout button\n *\n * @example\n * ```tsx\n * import { UserProfileMenu } from '@burdenoff/microfe-auth';\n *\n * function AppHeader() {\n * return (\n * <div className=\"header\">\n * <UserProfileMenu\n * onSwitchWorkspace={() => setShowWorkspaceSwitcher(true)}\n * position=\"right\"\n * />\n * </div>\n * );\n * }\n * ```\n */\nexport function UserProfileMenu({\n onSwitchWorkspace,\n onLogout,\n position = \"right\",\n}: UserProfileMenuProps) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const [isOpen, setIsOpen] = useState(false);\n const menuRef = useRef<HTMLDivElement>(null);\n const navigate = useNavigate();\n\n const user = useAuthStore((state) => state.user);\n const currentWorkspaceId = useAuthStore((state) => state.currentWorkspaceId);\n\n // Close dropdown when clicking outside\n useEffect(() => {\n function handleClickOutside(event: MouseEvent) {\n if (menuRef.current && !menuRef.current.contains(event.target as Node)) {\n setIsOpen(false);\n }\n }\n\n if (isOpen) {\n document.addEventListener(\"mousedown\", handleClickOutside);\n return () =>\n document.removeEventListener(\"mousedown\", handleClickOutside);\n }\n }, [isOpen]);\n\n const handleLogout = () => {\n setIsOpen(false);\n onLogout();\n };\n\n const handleSwitchWorkspace = () => {\n setIsOpen(false);\n onSwitchWorkspace?.();\n };\n\n if (!user) {\n return null;\n }\n\n // Get user initials for avatar\n const getInitials = () => {\n if (user.firstName && user.lastName) {\n return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase();\n }\n if (user.name) {\n const parts = user.name.split(\" \");\n if (parts.length >= 2) {\n return `${parts[0][0]}${parts[1][0]}`.toUpperCase();\n }\n return user.name.substring(0, 2).toUpperCase();\n }\n if (user.username) {\n return user.username.substring(0, 2).toUpperCase();\n }\n return user.email.substring(0, 2).toUpperCase();\n };\n\n const displayName =\n user.name ||\n `${user.firstName || \"\"} ${user.lastName || \"\"}`.trim() ||\n user.username;\n\n return (\n <div className=\"relative\" ref={menuRef}>\n {/* Profile Button */}\n <button\n type=\"button\"\n onClick={() => setIsOpen(!isOpen)}\n className=\"flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-bg-sunken transition-colors\"\n aria-label=\"User menu\"\n aria-expanded={isOpen}\n >\n {/* Avatar */}\n <div className=\"size-8 rounded-full bg-accent-blue flex items-center justify-center text-white text-sm font-medium overflow-hidden\">\n {user.avatar ? (\n <img\n src={user.avatar}\n alt={tr(\"auth.profileMenu.avatarAlt\", \"Profile picture\")}\n className=\"size-full object-cover\"\n />\n ) : (\n getInitials()\n )}\n </div>\n\n {/* User Name (optional, hide on small screens) */}\n <span className=\"hidden sm:block text-sm font-medium text-text-secondary\">\n {displayName}\n </span>\n\n {/* Chevron */}\n <ChevronDown\n className={`size-4 text-text-secondary transition-transform ${\n isOpen ? \"rotate-180\" : \"\"\n }`}\n />\n </button>\n\n {/* Dropdown Menu */}\n {isOpen && (\n <div\n className={`absolute top-full mt-2 w-64 bg-bg-surface rounded-lg shadow-lg border border-border-subtle py-2 z-50 ${\n position === \"right\" ? \"right-0\" : \"left-0\"\n }`}\n >\n {/* User Info Section */}\n <div className=\"px-4 py-3 border-b border-border-subtle\">\n <p className=\"text-sm font-medium text-text-primary truncate\">\n {displayName}\n </p>\n <p className=\"text-xs text-text-secondary truncate\">{user.email}</p>\n {currentWorkspaceId && (\n <p className=\"text-xs text-text-tertiary mt-1 truncate\">\n Workspace: {currentWorkspaceId.substring(0, 8)}...\n </p>\n )}\n </div>\n\n {/* Menu Items */}\n <div className=\"py-1\">\n {/* Switch Workspace */}\n {onSwitchWorkspace && (\n <button\n type=\"button\"\n onClick={handleSwitchWorkspace}\n className=\"w-full flex items-center gap-3 px-4 py-2 text-sm text-text-secondary hover:bg-bg-sunken transition-colors\"\n >\n <Building2 className=\"size-4\" />\n <span>\n {tr(\"auth.profileMenu.switchWorkspace\", \"Switch Workspace\")}\n </span>\n </button>\n )}\n\n {/* Add Account */}\n <button\n type=\"button\"\n onClick={() => {\n setIsOpen(false);\n const returnTo = encodeURIComponent(\n window.location.pathname + window.location.search,\n );\n navigate(`/auth/login?mode=add-account&returnTo=${returnTo}`);\n }}\n className=\"w-full flex items-center gap-3 px-4 py-2 text-sm text-text-secondary hover:bg-bg-sunken transition-colors\"\n >\n <UserPlus className=\"size-4\" />\n <span>{tr(\"auth.profileMenu.addAccount\", \"Add account\")}</span>\n </button>\n\n {/* Logout */}\n <button\n type=\"button\"\n onClick={handleLogout}\n className=\"w-full flex items-center gap-3 px-4 py-2 text-sm text-status-error-text hover:bg-status-error-bg-subtle transition-colors\"\n >\n <LogOut className=\"size-4\" />\n <span>{tr(\"auth.profileMenu.logout\", \"Logout\")}</span>\n </button>\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -49,7 +49,7 @@ function c({ isOpen: c, onClose: l, workspaces: u, projects: d = s, onSelect: f,
49
49
  /* @__PURE__ */ o("div", {
50
50
  className: "space-y-2",
51
51
  children: [/* @__PURE__ */ a("h2", {
52
- className: "text-2xl font-semibold text-foreground",
52
+ className: "text-2xl font-semibold text-text-primary",
53
53
  children: g("auth.workspaceSelector.title", "Select Your Workspace")
54
54
  }), /* @__PURE__ */ a("p", {
55
55
  className: "text-sm text-text-secondary",
@@ -91,7 +91,7 @@ function c({ isOpen: c, onClose: l, workspaces: u, projects: d = s, onSelect: f,
91
91
  id: _,
92
92
  value: b || "",
93
93
  onChange: (e) => j(e.target.value),
94
- className: "w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-foreground focus:border-primary focus:ring-2 focus:ring-primary/20",
94
+ className: "w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-text-primary focus:border-primary focus:ring-2 focus:ring-primary/20",
95
95
  children: [/* @__PURE__ */ a("option", {
96
96
  value: "",
97
97
  children: g("auth.workspaceSelector.selectOrg", "Select organization...")
@@ -134,7 +134,7 @@ function c({ isOpen: c, onClose: l, workspaces: u, projects: d = s, onSelect: f,
134
134
  value: S || "",
135
135
  onChange: (e) => M(e.target.value),
136
136
  disabled: !b && E.length > 1,
137
- className: "w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-foreground focus:border-primary focus:ring-2 focus:ring-primary/20 disabled:opacity-50 disabled:cursor-not-allowed",
137
+ className: "w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-text-primary focus:border-primary focus:ring-2 focus:ring-primary/20 disabled:opacity-50 disabled:cursor-not-allowed",
138
138
  children: [/* @__PURE__ */ a("option", {
139
139
  value: "",
140
140
  children: g("auth.workspaceSelector.selectWorkspace", "Select workspace...")
@@ -174,7 +174,7 @@ function c({ isOpen: c, onClose: l, workspaces: u, projects: d = s, onSelect: f,
174
174
  id: y,
175
175
  value: w || "",
176
176
  onChange: (e) => N(e.target.value),
177
- className: "w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-foreground focus:border-primary focus:ring-2 focus:ring-primary/20",
177
+ className: "w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-text-primary focus:border-primary focus:ring-2 focus:ring-primary/20",
178
178
  children: [/* @__PURE__ */ a("option", {
179
179
  value: "",
180
180
  children: g("auth.workspaceSelector.noProject", "No project selected")
@@ -187,7 +187,7 @@ function c({ isOpen: c, onClose: l, workspaces: u, projects: d = s, onSelect: f,
187
187
  k && /* @__PURE__ */ o("div", {
188
188
  className: "rounded-lg bg-bg-sunken p-4 space-y-2",
189
189
  children: [/* @__PURE__ */ a("p", {
190
- className: "text-sm font-medium text-foreground",
190
+ className: "text-sm font-medium text-text-primary",
191
191
  children: g("auth.workspaceSelector.selected", "Selected:")
192
192
  }), /* @__PURE__ */ o("div", {
193
193
  className: "text-sm text-text-secondary space-y-1",
@@ -236,7 +236,7 @@ function c({ isOpen: c, onClose: l, workspaces: u, projects: d = s, onSelect: f,
236
236
  type: "button",
237
237
  onClick: P,
238
238
  disabled: !I || p,
239
- className: "px-4 py-2 text-sm font-medium text-primary-foreground bg-primary hover:bg-primary/90 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
239
+ className: "px-4 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bg/90 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
240
240
  children: p ? g("auth.workspaceSelector.loading", "Loading...") : g("auth.workspaceSelector.continue", "Continue")
241
241
  })]
242
242
  })
@@ -1 +1 @@
1
- {"version":3,"file":"WorkspaceSelector.js","names":[],"sources":["../../../../src/auth/components/workspace/WorkspaceSelector.tsx"],"sourcesContent":["import { useState, useMemo, useEffect, useId } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport type { Workspace } from \"../../types\";\n\n/**\n * Organization interface (grouped from workspaces)\n */\nexport interface Organization {\n id: string;\n name: string;\n workspaceCount: number;\n}\n\n/**\n * Project interface (optional, for future use)\n */\nexport interface Project {\n id: string;\n name: string;\n workspaceId: string;\n}\n\nexport interface WorkspaceSelectorProps {\n /**\n * Control modal visibility\n */\n isOpen: boolean;\n\n /**\n * Callback when modal is closed without selection\n */\n onClose: () => void;\n\n /**\n * List of workspaces to choose from\n */\n workspaces: Workspace[];\n\n /**\n * Optional list of projects (filtered by workspace)\n */\n projects?: Project[];\n\n /**\n * Callback when workspace is selected and confirmed\n */\n onSelect: (workspace: Workspace, project?: Project) => void;\n\n /**\n * Loading state while issuing workspace token\n */\n isLoading?: boolean;\n\n /**\n * Error message to display\n */\n error?: string | null;\n}\n\n/**\n * WorkspaceSelector Component\n *\n * Multi-level workspace selection modal for use across all Burdenoff products.\n *\n * Features:\n * - Three-tier selection: Organization → Workspace → Project (optional)\n * - Auto-select if only one option at each level\n * - Loading and error states\n * - Clean, accessible UI\n *\n * Usage:\n * ```tsx\n * import { WorkspaceSelector } from '@burdenoff/microfe-auth';\n *\n * <WorkspaceSelector\n * isOpen={showSelector}\n * onClose={() => setShowSelector(false)}\n * workspaces={workspaces}\n * onSelect={async (workspace) => {\n * await issueWorkspaceToken(workspace.id);\n * navigate('/');\n * }}\n * />\n * ```\n */\nconst EMPTY_PROJECTS: readonly Project[] = [];\n\nexport function WorkspaceSelector({\n isOpen,\n onClose,\n workspaces,\n projects = EMPTY_PROJECTS as Project[],\n onSelect,\n isLoading = false,\n error = null,\n}: WorkspaceSelectorProps) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n // Generated ids for label-control pairing\n const orgSelectId = useId();\n const workspaceSelectId = useId();\n const projectSelectId = useId();\n // Selection state\n const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);\n const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string | null>(\n null,\n );\n const [selectedProjectId, setSelectedProjectId] = useState<string | null>(\n null,\n );\n\n // Group workspaces by organization\n const organizations = useMemo<Organization[]>(() => {\n const orgMap = new Map<string, { name: string; count: number }>();\n\n workspaces.forEach((workspace) => {\n const orgId = workspace.organizationId || \"default\";\n if (!orgMap.has(orgId)) {\n // Use org ID for name since workspace doesn't have organizationName\n orgMap.set(orgId, {\n name:\n orgId === \"default\"\n ? \"Default\"\n : `Organization ${orgId.slice(0, 8)}`,\n count: 0,\n });\n }\n orgMap.get(orgId)!.count++;\n });\n\n return Array.from(orgMap.entries()).map(([id, { name, count }]) => ({\n id,\n name,\n workspaceCount: count,\n }));\n }, [workspaces]);\n\n // Filter workspaces by selected organization\n const filteredWorkspaces = useMemo(() => {\n if (!selectedOrgId) return workspaces;\n return workspaces.filter(\n (w) => (w.organizationId || \"default\") === selectedOrgId,\n );\n }, [workspaces, selectedOrgId]);\n\n // Filter projects by selected workspace\n const filteredProjects = useMemo(() => {\n if (!selectedWorkspaceId) return [];\n return projects.filter((p) => p.workspaceId === selectedWorkspaceId);\n }, [projects, selectedWorkspaceId]);\n\n // Get selected workspace object\n const selectedWorkspace = useMemo(() => {\n return workspaces.find((w) => w.id === selectedWorkspaceId);\n }, [workspaces, selectedWorkspaceId]);\n\n // Get selected project object\n const selectedProject = useMemo(() => {\n return projects.find((p) => p.id === selectedProjectId);\n }, [projects, selectedProjectId]);\n\n // Auto-select if only one organization\n useEffect(() => {\n if (organizations.length === 1 && !selectedOrgId) {\n setSelectedOrgId(organizations[0].id);\n }\n }, [organizations, selectedOrgId]);\n\n // Auto-select if only one workspace\n useEffect(() => {\n if (filteredWorkspaces.length === 1 && !selectedWorkspaceId) {\n setSelectedWorkspaceId(filteredWorkspaces[0].id);\n }\n }, [filteredWorkspaces, selectedWorkspaceId]);\n\n // Handle organization selection\n const handleOrgChange = (orgId: string) => {\n setSelectedOrgId(orgId);\n setSelectedWorkspaceId(null);\n setSelectedProjectId(null);\n };\n\n // Handle workspace selection\n const handleWorkspaceChange = (workspaceId: string) => {\n setSelectedWorkspaceId(workspaceId);\n setSelectedProjectId(null);\n };\n\n // Handle project selection\n const handleProjectChange = (projectId: string) => {\n setSelectedProjectId(projectId);\n };\n\n // Handle continue button\n const handleContinue = () => {\n if (!selectedWorkspace) return;\n onSelect(selectedWorkspace, selectedProject);\n };\n\n // Handle cancel\n const handleCancel = () => {\n setSelectedOrgId(null);\n setSelectedWorkspaceId(null);\n setSelectedProjectId(null);\n onClose();\n };\n\n // Check if continue button should be enabled\n const canContinue = selectedWorkspaceId !== null;\n\n if (!isOpen) return null;\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n {/* Backdrop */}\n <div\n className=\"absolute inset-0 bg-black/50\"\n onClick={handleCancel}\n aria-hidden=\"true\"\n />\n\n {/* Modal */}\n <div className=\"relative bg-bg-surface rounded-2xl shadow-xl max-w-2xl w-full mx-4 p-6 space-y-6\">\n {/* Header */}\n <div className=\"space-y-2\">\n <h2 className=\"text-2xl font-semibold text-foreground\">\n {tr(\"auth.workspaceSelector.title\", \"Select Your Workspace\")}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n \"auth.workspaceSelector.subtitle\",\n \"Choose the workspace you want to work in. You can switch workspaces later from the header.\",\n )}\n </p>\n </div>\n\n {/* Error display */}\n {error && (\n <div className=\"rounded-lg bg-status-error-bg-subtle p-4 text-sm text-status-error-text\">\n <p className=\"font-medium\">\n {tr(\"auth.workspaceSelector.error\", \"Error\")}\n </p>\n <p>{error}</p>\n </div>\n )}\n\n {/* Form */}\n <div className=\"space-y-4\">\n {/* Organization selection */}\n {organizations.length > 1 && (\n <div className=\"space-y-2\">\n <label\n htmlFor={orgSelectId}\n className=\"block text-sm font-medium text-text-secondary\"\n >\n <span className=\"flex items-center gap-2\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n Organization\n </span>\n </label>\n <select\n id={orgSelectId}\n value={selectedOrgId || \"\"}\n onChange={(e) => handleOrgChange(e.target.value)}\n className=\"w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-foreground focus:border-primary focus:ring-2 focus:ring-primary/20\"\n >\n <option value=\"\">\n {tr(\n \"auth.workspaceSelector.selectOrg\",\n \"Select organization...\",\n )}\n </option>\n {organizations.map((org) => (\n <option key={org.id} value={org.id}>\n {org.name} ({org.workspaceCount} workspace\n {org.workspaceCount !== 1 ? \"s\" : \"\"})\n </option>\n ))}\n </select>\n </div>\n )}\n\n {/* Workspace selection */}\n <div className=\"space-y-2\">\n <label\n htmlFor={workspaceSelectId}\n className=\"block text-sm font-medium text-text-secondary\"\n >\n <span className=\"flex items-center gap-2\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z\"\n />\n </svg>\n Workspace\n </span>\n </label>\n <select\n id={workspaceSelectId}\n value={selectedWorkspaceId || \"\"}\n onChange={(e) => handleWorkspaceChange(e.target.value)}\n disabled={!selectedOrgId && organizations.length > 1}\n className=\"w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-foreground focus:border-primary focus:ring-2 focus:ring-primary/20 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n <option value=\"\">\n {tr(\n \"auth.workspaceSelector.selectWorkspace\",\n \"Select workspace...\",\n )}\n </option>\n {filteredWorkspaces.map((workspace) => (\n <option key={workspace.id} value={workspace.id}>\n {workspace.name} ({workspace.slug})\n </option>\n ))}\n </select>\n </div>\n\n {/* Project selection (optional) */}\n {filteredProjects.length > 0 && (\n <div className=\"space-y-2\">\n <label\n htmlFor={projectSelectId}\n className=\"block text-sm font-medium text-text-secondary\"\n >\n <span className=\"flex items-center gap-2\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\"\n />\n </svg>\n Project (Optional)\n </span>\n </label>\n <select\n id={projectSelectId}\n value={selectedProjectId || \"\"}\n onChange={(e) => handleProjectChange(e.target.value)}\n className=\"w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-foreground focus:border-primary focus:ring-2 focus:ring-primary/20\"\n >\n <option value=\"\">\n {tr(\n \"auth.workspaceSelector.noProject\",\n \"No project selected\",\n )}\n </option>\n {filteredProjects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n )}\n\n {/* Selection summary */}\n {selectedWorkspace && (\n <div className=\"rounded-lg bg-bg-sunken p-4 space-y-2\">\n <p className=\"text-sm font-medium text-foreground\">\n {tr(\"auth.workspaceSelector.selected\", \"Selected:\")}\n </p>\n <div className=\"text-sm text-text-secondary space-y-1\">\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-3\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z\"\n />\n </svg>\n <span>{selectedWorkspace.name}</span>\n </div>\n {selectedProject && (\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-3\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\"\n />\n </svg>\n <span>{selectedProject.name}</span>\n </div>\n )}\n </div>\n </div>\n )}\n </div>\n\n {/* Footer */}\n <div className=\"flex items-center justify-end gap-3 pt-4 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={handleCancel}\n disabled={isLoading}\n className=\"px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-sunken rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={handleContinue}\n disabled={!canContinue || isLoading}\n className=\"px-4 py-2 text-sm font-medium text-primary-foreground bg-primary hover:bg-primary/90 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isLoading\n ? tr(\"auth.workspaceSelector.loading\", \"Loading...\")\n : tr(\"auth.workspaceSelector.continue\", \"Continue\")}\n </button>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"WorkspaceSelector.js","names":[],"sources":["../../../../src/auth/components/workspace/WorkspaceSelector.tsx"],"sourcesContent":["import { useState, useMemo, useEffect, useId } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport type { Workspace } from \"../../types\";\n\n/**\n * Organization interface (grouped from workspaces)\n */\nexport interface Organization {\n id: string;\n name: string;\n workspaceCount: number;\n}\n\n/**\n * Project interface (optional, for future use)\n */\nexport interface Project {\n id: string;\n name: string;\n workspaceId: string;\n}\n\nexport interface WorkspaceSelectorProps {\n /**\n * Control modal visibility\n */\n isOpen: boolean;\n\n /**\n * Callback when modal is closed without selection\n */\n onClose: () => void;\n\n /**\n * List of workspaces to choose from\n */\n workspaces: Workspace[];\n\n /**\n * Optional list of projects (filtered by workspace)\n */\n projects?: Project[];\n\n /**\n * Callback when workspace is selected and confirmed\n */\n onSelect: (workspace: Workspace, project?: Project) => void;\n\n /**\n * Loading state while issuing workspace token\n */\n isLoading?: boolean;\n\n /**\n * Error message to display\n */\n error?: string | null;\n}\n\n/**\n * WorkspaceSelector Component\n *\n * Multi-level workspace selection modal for use across all Burdenoff products.\n *\n * Features:\n * - Three-tier selection: Organization → Workspace → Project (optional)\n * - Auto-select if only one option at each level\n * - Loading and error states\n * - Clean, accessible UI\n *\n * Usage:\n * ```tsx\n * import { WorkspaceSelector } from '@burdenoff/microfe-auth';\n *\n * <WorkspaceSelector\n * isOpen={showSelector}\n * onClose={() => setShowSelector(false)}\n * workspaces={workspaces}\n * onSelect={async (workspace) => {\n * await issueWorkspaceToken(workspace.id);\n * navigate('/');\n * }}\n * />\n * ```\n */\nconst EMPTY_PROJECTS: readonly Project[] = [];\n\nexport function WorkspaceSelector({\n isOpen,\n onClose,\n workspaces,\n projects = EMPTY_PROJECTS as Project[],\n onSelect,\n isLoading = false,\n error = null,\n}: WorkspaceSelectorProps) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n // Generated ids for label-control pairing\n const orgSelectId = useId();\n const workspaceSelectId = useId();\n const projectSelectId = useId();\n // Selection state\n const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);\n const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string | null>(\n null,\n );\n const [selectedProjectId, setSelectedProjectId] = useState<string | null>(\n null,\n );\n\n // Group workspaces by organization\n const organizations = useMemo<Organization[]>(() => {\n const orgMap = new Map<string, { name: string; count: number }>();\n\n workspaces.forEach((workspace) => {\n const orgId = workspace.organizationId || \"default\";\n if (!orgMap.has(orgId)) {\n // Use org ID for name since workspace doesn't have organizationName\n orgMap.set(orgId, {\n name:\n orgId === \"default\"\n ? \"Default\"\n : `Organization ${orgId.slice(0, 8)}`,\n count: 0,\n });\n }\n orgMap.get(orgId)!.count++;\n });\n\n return Array.from(orgMap.entries()).map(([id, { name, count }]) => ({\n id,\n name,\n workspaceCount: count,\n }));\n }, [workspaces]);\n\n // Filter workspaces by selected organization\n const filteredWorkspaces = useMemo(() => {\n if (!selectedOrgId) return workspaces;\n return workspaces.filter(\n (w) => (w.organizationId || \"default\") === selectedOrgId,\n );\n }, [workspaces, selectedOrgId]);\n\n // Filter projects by selected workspace\n const filteredProjects = useMemo(() => {\n if (!selectedWorkspaceId) return [];\n return projects.filter((p) => p.workspaceId === selectedWorkspaceId);\n }, [projects, selectedWorkspaceId]);\n\n // Get selected workspace object\n const selectedWorkspace = useMemo(() => {\n return workspaces.find((w) => w.id === selectedWorkspaceId);\n }, [workspaces, selectedWorkspaceId]);\n\n // Get selected project object\n const selectedProject = useMemo(() => {\n return projects.find((p) => p.id === selectedProjectId);\n }, [projects, selectedProjectId]);\n\n // Auto-select if only one organization\n useEffect(() => {\n if (organizations.length === 1 && !selectedOrgId) {\n setSelectedOrgId(organizations[0].id);\n }\n }, [organizations, selectedOrgId]);\n\n // Auto-select if only one workspace\n useEffect(() => {\n if (filteredWorkspaces.length === 1 && !selectedWorkspaceId) {\n setSelectedWorkspaceId(filteredWorkspaces[0].id);\n }\n }, [filteredWorkspaces, selectedWorkspaceId]);\n\n // Handle organization selection\n const handleOrgChange = (orgId: string) => {\n setSelectedOrgId(orgId);\n setSelectedWorkspaceId(null);\n setSelectedProjectId(null);\n };\n\n // Handle workspace selection\n const handleWorkspaceChange = (workspaceId: string) => {\n setSelectedWorkspaceId(workspaceId);\n setSelectedProjectId(null);\n };\n\n // Handle project selection\n const handleProjectChange = (projectId: string) => {\n setSelectedProjectId(projectId);\n };\n\n // Handle continue button\n const handleContinue = () => {\n if (!selectedWorkspace) return;\n onSelect(selectedWorkspace, selectedProject);\n };\n\n // Handle cancel\n const handleCancel = () => {\n setSelectedOrgId(null);\n setSelectedWorkspaceId(null);\n setSelectedProjectId(null);\n onClose();\n };\n\n // Check if continue button should be enabled\n const canContinue = selectedWorkspaceId !== null;\n\n if (!isOpen) return null;\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n {/* Backdrop */}\n <div\n className=\"absolute inset-0 bg-black/50\"\n onClick={handleCancel}\n aria-hidden=\"true\"\n />\n\n {/* Modal */}\n <div className=\"relative bg-bg-surface rounded-2xl shadow-xl max-w-2xl w-full mx-4 p-6 space-y-6\">\n {/* Header */}\n <div className=\"space-y-2\">\n <h2 className=\"text-2xl font-semibold text-text-primary\">\n {tr(\"auth.workspaceSelector.title\", \"Select Your Workspace\")}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n \"auth.workspaceSelector.subtitle\",\n \"Choose the workspace you want to work in. You can switch workspaces later from the header.\",\n )}\n </p>\n </div>\n\n {/* Error display */}\n {error && (\n <div className=\"rounded-lg bg-status-error-bg-subtle p-4 text-sm text-status-error-text\">\n <p className=\"font-medium\">\n {tr(\"auth.workspaceSelector.error\", \"Error\")}\n </p>\n <p>{error}</p>\n </div>\n )}\n\n {/* Form */}\n <div className=\"space-y-4\">\n {/* Organization selection */}\n {organizations.length > 1 && (\n <div className=\"space-y-2\">\n <label\n htmlFor={orgSelectId}\n className=\"block text-sm font-medium text-text-secondary\"\n >\n <span className=\"flex items-center gap-2\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n Organization\n </span>\n </label>\n <select\n id={orgSelectId}\n value={selectedOrgId || \"\"}\n onChange={(e) => handleOrgChange(e.target.value)}\n className=\"w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-text-primary focus:border-primary focus:ring-2 focus:ring-primary/20\"\n >\n <option value=\"\">\n {tr(\n \"auth.workspaceSelector.selectOrg\",\n \"Select organization...\",\n )}\n </option>\n {organizations.map((org) => (\n <option key={org.id} value={org.id}>\n {org.name} ({org.workspaceCount} workspace\n {org.workspaceCount !== 1 ? \"s\" : \"\"})\n </option>\n ))}\n </select>\n </div>\n )}\n\n {/* Workspace selection */}\n <div className=\"space-y-2\">\n <label\n htmlFor={workspaceSelectId}\n className=\"block text-sm font-medium text-text-secondary\"\n >\n <span className=\"flex items-center gap-2\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z\"\n />\n </svg>\n Workspace\n </span>\n </label>\n <select\n id={workspaceSelectId}\n value={selectedWorkspaceId || \"\"}\n onChange={(e) => handleWorkspaceChange(e.target.value)}\n disabled={!selectedOrgId && organizations.length > 1}\n className=\"w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-text-primary focus:border-primary focus:ring-2 focus:ring-primary/20 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n <option value=\"\">\n {tr(\n \"auth.workspaceSelector.selectWorkspace\",\n \"Select workspace...\",\n )}\n </option>\n {filteredWorkspaces.map((workspace) => (\n <option key={workspace.id} value={workspace.id}>\n {workspace.name} ({workspace.slug})\n </option>\n ))}\n </select>\n </div>\n\n {/* Project selection (optional) */}\n {filteredProjects.length > 0 && (\n <div className=\"space-y-2\">\n <label\n htmlFor={projectSelectId}\n className=\"block text-sm font-medium text-text-secondary\"\n >\n <span className=\"flex items-center gap-2\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\"\n />\n </svg>\n Project (Optional)\n </span>\n </label>\n <select\n id={projectSelectId}\n value={selectedProjectId || \"\"}\n onChange={(e) => handleProjectChange(e.target.value)}\n className=\"w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-text-primary focus:border-primary focus:ring-2 focus:ring-primary/20\"\n >\n <option value=\"\">\n {tr(\n \"auth.workspaceSelector.noProject\",\n \"No project selected\",\n )}\n </option>\n {filteredProjects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n )}\n\n {/* Selection summary */}\n {selectedWorkspace && (\n <div className=\"rounded-lg bg-bg-sunken p-4 space-y-2\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr(\"auth.workspaceSelector.selected\", \"Selected:\")}\n </p>\n <div className=\"text-sm text-text-secondary space-y-1\">\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-3\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z\"\n />\n </svg>\n <span>{selectedWorkspace.name}</span>\n </div>\n {selectedProject && (\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-3\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\"\n />\n </svg>\n <span>{selectedProject.name}</span>\n </div>\n )}\n </div>\n </div>\n )}\n </div>\n\n {/* Footer */}\n <div className=\"flex items-center justify-end gap-3 pt-4 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={handleCancel}\n disabled={isLoading}\n className=\"px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-sunken rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={handleContinue}\n disabled={!canContinue || isLoading}\n className=\"px-4 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bg/90 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isLoading\n ? tr(\"auth.workspaceSelector.loading\", \"Loading...\")\n : tr(\"auth.workspaceSelector.continue\", \"Continue\")}\n </button>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -122,7 +122,7 @@ function p() {
122
122
  className: "flex items-start gap-4",
123
123
  children: [
124
124
  /* @__PURE__ */ c("div", {
125
- className: "flex size-10 shrink-0 items-center justify-center rounded-full bg-bg-muted text-xs font-semibold text-text-secondary",
125
+ className: "flex size-10 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-secondary",
126
126
  "aria-hidden": "true",
127
127
  children: k(e.action)
128
128
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"AuditLogPage.js","names":[],"sources":["../../../src/auth/pages/AuditLogPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { useGatewayClient } from \"@burdenoff/fe-libs/shared/graphql\";\nimport { useFeatureFlagDetails } from \"@burdenoff/fe-libs/shared/feature-flags\";\nimport { formatDateTime } from \"../utils/formatters\";\nimport { AlertIcon, SpinnerIcon } from \"../components/AuthIcons\";\nimport {\n useAuthAuditLogsQuery,\n type AuditLogEntry_AuthAuditLogFragment,\n} from \"../../generated/global-operations\";\nimport { useCreateAuthExportJobMutation } from \"../../generated/wspace-operations\";\n\nconst PAGE_SIZE = 20;\n\n/**\n * Security audit log page with pagination and export\n *\n * Fragment: AuditLogEntry_AuthAuditLog (graphql/operations/global/fragments/AuditLogEntry.fragment.graphql)\n * Query: AuthAuditLogs (graphql/operations/global/queries/auditLog.graphql)\n *\n * TODO: When authAuditLogs backend migrates to AuthAuditLogConnection type,\n * update this query to use: authAuditLogs { auditLogs: [...], totalCount, hasMore }\n * and remove the client-side hasMore approximation below.\n */\nexport function AuditLogPage() {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n\n const [page, setPage] = useState(1);\n const offset = (page - 1) * PAGE_SIZE;\n\n // Default-deny: only show when the flag is explicitly enabled in the system.\n // useFeatureFlag uses default-allow (returns true when not found), so we use\n // useFeatureFlagDetails + explicit value check instead.\n const auditLogFlag = useFeatureFlagDetails(\"auth.audit-log\");\n const showAuditLog = auditLogFlag?.value === true;\n\n const { data, loading, error } = useAuthAuditLogsQuery({\n skip: !showAuditLog,\n fetchPolicy: \"cache-and-network\",\n variables: { pagination: { limit: PAGE_SIZE, offset } },\n });\n\n const auditLogs = data?.authAuditLogs ?? [];\n // Approximate hasMore: if we received a full page there may be more\n const hasMore = auditLogs.length === PAGE_SIZE;\n\n const wspaceClient = useGatewayClient(\"workspace\");\n const [createExportJob, { loading: isExporting }] =\n useCreateAuthExportJobMutation({ client: wspaceClient });\n const [exportError, setExportError] = useState<string | null>(null);\n\n const handleExport = async () => {\n setExportError(null);\n try {\n await createExportJob({\n variables: {\n input: {\n name: \"Audit Log Export\",\n // permission_audit_logs covers auth audit logs in the export system\n entityTypes: [\"permission_audit_logs\"],\n format: \"CSV\",\n exportScope: \"INDIVIDUAL\",\n includeMetadata: true,\n },\n },\n });\n } catch (err) {\n setExportError(\n err instanceof Error ? err.message : \"Failed to start export\",\n );\n }\n };\n\n const getEventMarker = (eventType: string) => {\n switch (eventType) {\n case \"LOGIN_SUCCESS\":\n return \"OK\";\n case \"LOGIN_FAILURE\":\n return \"ER\";\n case \"LOGOUT\":\n return \"LO\";\n case \"PASSWORD_CHANGE\":\n return \"PW\";\n case \"MFA_ENABLED\":\n case \"MFA_VERIFIED\":\n return \"MF\";\n case \"ANOMALY_DETECTED\":\n return \"AL\";\n default:\n return \"AU\";\n }\n };\n\n if (!showAuditLog) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <div className=\"text-center py-16 border-2 border-dashed border-border-subtle rounded-xl\">\n <div className=\"size-16 mx-auto mb-4 rounded-full bg-status-warning-bg-subtle flex items-center justify-center\">\n <AlertIcon\n className=\"size-8 text-status-warning-text\"\n aria-hidden=\"true\"\n />\n </div>\n <h2 className=\"text-xl font-semibold text-text-primary mb-2\">\n {tr(\"auth.auditLog.notAvailable\", \"Page Not Available\")}\n </h2>\n <p className=\"text-text-secondary\">\n {tr(\n \"auth.auditLog.notEnabled\",\n \"The Security Audit Log is not enabled for your account.\",\n )}\n </p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n {/* Header */}\n <div className=\"flex items-center justify-between mb-8\">\n <h1 className=\"text-3xl font-semibold text-text-primary\">\n {tr(\"auth.auditLog.title\", \"Security Audit Log\")}\n </h1>\n <button\n type=\"button\"\n onClick={() => void handleExport()}\n disabled={isExporting}\n aria-label={tr(\"auth.auditLog.exportAriaLabel\", \"Export audit log\")}\n className=\"flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium\"\n >\n {isExporting ? (\n <>\n <SpinnerIcon className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n {tr(\"auth.auditLog.exporting\", \"Exporting...\")}\n </>\n ) : (\n <>\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4\"\n />\n </svg>\n {tr(\"auth.auditLog.export\", \"Export\")}\n </>\n )}\n </button>\n </div>\n\n {/* Export error */}\n {exportError && (\n <div\n role=\"alert\"\n className=\"mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\"\n >\n <AlertIcon\n className=\"size-5 text-status-error-text flex-shrink-0 mt-0.5\"\n aria-hidden=\"true\"\n />\n <p className=\"text-sm text-status-error-text\">{exportError}</p>\n </div>\n )}\n\n {loading ? (\n <div aria-live=\"polite\" aria-busy=\"true\" className=\"text-center py-12\">\n <SpinnerIcon\n className=\"size-8 mx-auto text-text-link animate-spin\"\n aria-hidden=\"true\"\n />\n <p className=\"mt-3 text-text-secondary\">\n {tr(\"auth.auditLog.loading\", \"Loading audit events...\")}\n </p>\n </div>\n ) : error ? (\n <div\n role=\"alert\"\n className=\"text-center py-12 border border-status-error-border rounded-xl bg-status-error-bg-subtle\"\n >\n <p className=\"text-status-error-text\">\n {tr(\"auth.auditLog.error\", \"Unable to load audit events\")}\n </p>\n </div>\n ) : auditLogs.length === 0 ? (\n <output className=\"block text-center py-12\">\n <p className=\"text-text-secondary\">\n {tr(\"auth.auditLog.noEvents\", \"No security events recorded\")}\n </p>\n </output>\n ) : (\n <>\n <div className=\"space-y-4\" aria-live=\"polite\" aria-busy={loading}>\n {auditLogs.map((event: AuditLogEntry_AuthAuditLogFragment) => (\n <div\n key={event.id}\n className=\"p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm\"\n >\n <div className=\"flex items-start gap-4\">\n <div\n className=\"flex size-10 shrink-0 items-center justify-center rounded-full bg-bg-muted text-xs font-semibold text-text-secondary\"\n aria-hidden=\"true\"\n >\n {getEventMarker(event.action)}\n </div>\n <div className=\"flex-1\">\n <h3 className=\"text-lg font-medium text-text-primary mb-1\">\n {event.action.replace(/_/g, \" \")}\n </h3>\n <p className=\"text-sm text-text-secondary mb-2\">\n <time dateTime={event.createdAt}>\n {formatDateTime(event.createdAt)}\n </time>\n </p>\n <p className=\"text-sm text-text-secondary\">\n {event.description ??\n tr(\n \"auth.auditLog.defaultDescription\",\n \"Authentication event\",\n )}\n {event.ipAddress && ` • ${event.ipAddress}`}\n </p>\n </div>\n {event.action.includes(\"FAILURE\") && (\n <output className=\"px-3 py-1 rounded-full bg-status-error-bg-subtle text-status-error-text text-sm font-medium\">\n {tr(\"auth.auditLog.failed\", \"Failed\")}\n </output>\n )}\n </div>\n </div>\n ))}\n </div>\n\n {/* Pagination */}\n <nav\n aria-label={tr(\n \"auth.auditLog.paginationAriaLabel\",\n \"Audit log pagination\",\n )}\n className=\"mt-6 flex items-center justify-between\"\n >\n <p className=\"text-sm text-text-secondary\">\n {tr(\"auth.auditLog.page\", \"Page\")} {page}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => setPage((prev) => Math.max(1, prev - 1))}\n disabled={page === 1}\n aria-label={tr(\n \"auth.auditLog.previousPageAriaLabel\",\n \"Go to previous page\",\n )}\n className=\"px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"auth.auditLog.previous\", \"Previous\")}\n </button>\n <button\n type=\"button\"\n onClick={() => setPage((prev) => prev + 1)}\n disabled={!hasMore}\n aria-label={tr(\n \"auth.auditLog.nextPageAriaLabel\",\n \"Go to next page\",\n )}\n className=\"px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"auth.auditLog.next\", \"Next\")}\n </button>\n </div>\n </nav>\n </>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAYA,IAAM,IAAY;AAYlB,SAAgB,IAAe;CAC7B,IAAM,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAa,EAAE,EAAI;AACzB,SAAO,MAAe,IAAM,IAAW;IAGnC,CAAC,GAAM,KAAW,EAAS,EAAE,EAC7B,KAAU,IAAO,KAAK,GAMtB,IADe,EAAsB,iBAAiB,EACzB,UAAU,IAEvC,EAAE,SAAM,YAAS,aAAU,EAAsB;EACrD,MAAM,CAAC;EACP,aAAa;EACb,WAAW,EAAE,YAAY;GAAE,OAAO;GAAW;GAAQ,EAAE;EACxD,CAAC,EAEI,IAAY,GAAM,iBAAiB,EAAE,EAErC,IAAU,EAAU,WAAW,GAG/B,CAAC,GAAiB,EAAE,SAAS,OACjC,EAA+B,EAAE,QAFd,EAAiB,YAAY,EAEO,CAAC,EACpD,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAE7D,IAAe,YAAY;AAC/B,IAAe,KAAK;AACpB,MAAI;AACF,SAAM,EAAgB,EACpB,WAAW,EACT,OAAO;IACL,MAAM;IAEN,aAAa,CAAC,wBAAwB;IACtC,QAAQ;IACR,aAAa;IACb,iBAAiB;IAClB,EACF,EACF,CAAC;WACK,GAAK;AACZ,KACE,aAAe,QAAQ,EAAI,UAAU,yBACtC;;IAIC,KAAkB,MAAsB;AAC5C,UAAQ,GAAR;GACE,KAAK,gBACH,QAAO;GACT,KAAK,gBACH,QAAO;GACT,KAAK,SACH,QAAO;GACT,KAAK,kBACH,QAAO;GACT,KAAK;GACL,KAAK,eACH,QAAO;GACT,KAAK,mBACH,QAAO;GACT,QACE,QAAO;;;AA4Bb,QAxBK,IAyBH,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,uBAAuB,qBAAqB;KAC7C,CAAA,EACL,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,KAAK,GAAc;KAClC,UAAU;KACV,cAAY,EAAG,iCAAiC,mBAAmB;KACnE,WAAU;eAET,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;MAAa,WAAU;MAAsB,eAAY;MAAS,CAAA,EACjE,EAAG,2BAA2B,eAAe,CAC7C,EAAA,CAAA,GAEH,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;MACE,WAAU;MACV,MAAK;MACL,SAAQ;MACR,QAAO;MACP,eAAY;gBAEZ,kBAAC,QAAD;OACE,eAAc;OACd,gBAAe;OACf,aAAa;OACb,GAAE;OACF,CAAA;MACE,CAAA,EACL,EAAG,wBAAwB,SAAS,CACpC,EAAA,CAAA;KAEE,CAAA,CACL;;GAGL,KACC,kBAAC,OAAD;IACE,MAAK;IACL,WAAU;cAFZ,CAIE,kBAAC,GAAD;KACE,WAAU;KACV,eAAY;KACZ,CAAA,EACF,kBAAC,KAAD;KAAG,WAAU;eAAkC;KAAgB,CAAA,CAC3D;;GAGP,IACC,kBAAC,OAAD;IAAK,aAAU;IAAS,aAAU;IAAO,WAAU;cAAnD,CACE,kBAAC,GAAD;KACE,WAAU;KACV,eAAY;KACZ,CAAA,EACF,kBAAC,KAAD;KAAG,WAAU;eACV,EAAG,yBAAyB,0BAA0B;KACrD,CAAA,CACA;QACJ,IACF,kBAAC,OAAD;IACE,MAAK;IACL,WAAU;cAEV,kBAAC,KAAD;KAAG,WAAU;eACV,EAAG,uBAAuB,8BAA8B;KACvD,CAAA;IACA,CAAA,GACJ,EAAU,WAAW,IACvB,kBAAC,UAAD;IAAQ,WAAU;cAChB,kBAAC,KAAD;KAAG,WAAU;eACV,EAAG,0BAA0B,8BAA8B;KAC1D,CAAA;IACG,CAAA,GAET,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;IAAY,aAAU;IAAS,aAAW;cACtD,EAAU,KAAK,MACd,kBAAC,OAAD;KAEE,WAAU;eAEV,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,OAAD;QACE,WAAU;QACV,eAAY;kBAEX,EAAe,EAAM,OAAO;QACzB,CAAA;OACN,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,MAAD;UAAI,WAAU;oBACX,EAAM,OAAO,QAAQ,MAAM,IAAI;UAC7B,CAAA;SACL,kBAAC,KAAD;UAAG,WAAU;oBACX,kBAAC,QAAD;WAAM,UAAU,EAAM;qBACnB,EAAe,EAAM,UAAU;WAC3B,CAAA;UACL,CAAA;SACJ,kBAAC,KAAD;UAAG,WAAU;oBAAb,CACG,EAAM,eACL,EACE,oCACA,uBACD,EACF,EAAM,aAAa,MAAM,EAAM,YAC9B;;SACA;;OACL,EAAM,OAAO,SAAS,UAAU,IAC/B,kBAAC,UAAD;QAAQ,WAAU;kBACf,EAAG,wBAAwB,SAAS;QAC9B,CAAA;OAEP;;KACF,EAlCC,EAAM,GAkCP,CACN;IACE,CAAA,EAGN,kBAAC,OAAD;IACE,cAAY,EACV,qCACA,uBACD;IACD,WAAU;cALZ,CAOE,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG,EAAG,sBAAsB,OAAO;MAAC;MAAE;MAClC;QACJ,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,GAAS,MAAS,KAAK,IAAI,GAAG,IAAO,EAAE,CAAC;MACvD,UAAU,MAAS;MACnB,cAAY,EACV,uCACA,sBACD;MACD,WAAU;gBAET,EAAG,0BAA0B,WAAW;MAClC,CAAA,EACT,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,GAAS,MAAS,IAAO,EAAE;MAC1C,UAAU,CAAC;MACX,cAAY,EACV,mCACA,kBACD;MACD,WAAU;gBAET,EAAG,sBAAsB,OAAO;MAC1B,CAAA,CACL;OACF;MACL,EAAA,CAAA;GAED;MAzLJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MACE,WAAU;MACV,eAAY;MACZ,CAAA;KACE,CAAA;IACN,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,8BAA8B,qBAAqB;KACpD,CAAA;IACL,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,4BACA,0DACD;KACC,CAAA;IACA;;EACF,CAAA"}
1
+ {"version":3,"file":"AuditLogPage.js","names":[],"sources":["../../../src/auth/pages/AuditLogPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { useGatewayClient } from \"@burdenoff/fe-libs/shared/graphql\";\nimport { useFeatureFlagDetails } from \"@burdenoff/fe-libs/shared/feature-flags\";\nimport { formatDateTime } from \"../utils/formatters\";\nimport { AlertIcon, SpinnerIcon } from \"../components/AuthIcons\";\nimport {\n useAuthAuditLogsQuery,\n type AuditLogEntry_AuthAuditLogFragment,\n} from \"../../generated/global-operations\";\nimport { useCreateAuthExportJobMutation } from \"../../generated/wspace-operations\";\n\nconst PAGE_SIZE = 20;\n\n/**\n * Security audit log page with pagination and export\n *\n * Fragment: AuditLogEntry_AuthAuditLog (graphql/operations/global/fragments/AuditLogEntry.fragment.graphql)\n * Query: AuthAuditLogs (graphql/operations/global/queries/auditLog.graphql)\n *\n * TODO: When authAuditLogs backend migrates to AuthAuditLogConnection type,\n * update this query to use: authAuditLogs { auditLogs: [...], totalCount, hasMore }\n * and remove the client-side hasMore approximation below.\n */\nexport function AuditLogPage() {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n\n const [page, setPage] = useState(1);\n const offset = (page - 1) * PAGE_SIZE;\n\n // Default-deny: only show when the flag is explicitly enabled in the system.\n // useFeatureFlag uses default-allow (returns true when not found), so we use\n // useFeatureFlagDetails + explicit value check instead.\n const auditLogFlag = useFeatureFlagDetails(\"auth.audit-log\");\n const showAuditLog = auditLogFlag?.value === true;\n\n const { data, loading, error } = useAuthAuditLogsQuery({\n skip: !showAuditLog,\n fetchPolicy: \"cache-and-network\",\n variables: { pagination: { limit: PAGE_SIZE, offset } },\n });\n\n const auditLogs = data?.authAuditLogs ?? [];\n // Approximate hasMore: if we received a full page there may be more\n const hasMore = auditLogs.length === PAGE_SIZE;\n\n const wspaceClient = useGatewayClient(\"workspace\");\n const [createExportJob, { loading: isExporting }] =\n useCreateAuthExportJobMutation({ client: wspaceClient });\n const [exportError, setExportError] = useState<string | null>(null);\n\n const handleExport = async () => {\n setExportError(null);\n try {\n await createExportJob({\n variables: {\n input: {\n name: \"Audit Log Export\",\n // permission_audit_logs covers auth audit logs in the export system\n entityTypes: [\"permission_audit_logs\"],\n format: \"CSV\",\n exportScope: \"INDIVIDUAL\",\n includeMetadata: true,\n },\n },\n });\n } catch (err) {\n setExportError(\n err instanceof Error ? err.message : \"Failed to start export\",\n );\n }\n };\n\n const getEventMarker = (eventType: string) => {\n switch (eventType) {\n case \"LOGIN_SUCCESS\":\n return \"OK\";\n case \"LOGIN_FAILURE\":\n return \"ER\";\n case \"LOGOUT\":\n return \"LO\";\n case \"PASSWORD_CHANGE\":\n return \"PW\";\n case \"MFA_ENABLED\":\n case \"MFA_VERIFIED\":\n return \"MF\";\n case \"ANOMALY_DETECTED\":\n return \"AL\";\n default:\n return \"AU\";\n }\n };\n\n if (!showAuditLog) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <div className=\"text-center py-16 border-2 border-dashed border-border-subtle rounded-xl\">\n <div className=\"size-16 mx-auto mb-4 rounded-full bg-status-warning-bg-subtle flex items-center justify-center\">\n <AlertIcon\n className=\"size-8 text-status-warning-text\"\n aria-hidden=\"true\"\n />\n </div>\n <h2 className=\"text-xl font-semibold text-text-primary mb-2\">\n {tr(\"auth.auditLog.notAvailable\", \"Page Not Available\")}\n </h2>\n <p className=\"text-text-secondary\">\n {tr(\n \"auth.auditLog.notEnabled\",\n \"The Security Audit Log is not enabled for your account.\",\n )}\n </p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n {/* Header */}\n <div className=\"flex items-center justify-between mb-8\">\n <h1 className=\"text-3xl font-semibold text-text-primary\">\n {tr(\"auth.auditLog.title\", \"Security Audit Log\")}\n </h1>\n <button\n type=\"button\"\n onClick={() => void handleExport()}\n disabled={isExporting}\n aria-label={tr(\"auth.auditLog.exportAriaLabel\", \"Export audit log\")}\n className=\"flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium\"\n >\n {isExporting ? (\n <>\n <SpinnerIcon className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n {tr(\"auth.auditLog.exporting\", \"Exporting...\")}\n </>\n ) : (\n <>\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4\"\n />\n </svg>\n {tr(\"auth.auditLog.export\", \"Export\")}\n </>\n )}\n </button>\n </div>\n\n {/* Export error */}\n {exportError && (\n <div\n role=\"alert\"\n className=\"mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\"\n >\n <AlertIcon\n className=\"size-5 text-status-error-text flex-shrink-0 mt-0.5\"\n aria-hidden=\"true\"\n />\n <p className=\"text-sm text-status-error-text\">{exportError}</p>\n </div>\n )}\n\n {loading ? (\n <div aria-live=\"polite\" aria-busy=\"true\" className=\"text-center py-12\">\n <SpinnerIcon\n className=\"size-8 mx-auto text-text-link animate-spin\"\n aria-hidden=\"true\"\n />\n <p className=\"mt-3 text-text-secondary\">\n {tr(\"auth.auditLog.loading\", \"Loading audit events...\")}\n </p>\n </div>\n ) : error ? (\n <div\n role=\"alert\"\n className=\"text-center py-12 border border-status-error-border rounded-xl bg-status-error-bg-subtle\"\n >\n <p className=\"text-status-error-text\">\n {tr(\"auth.auditLog.error\", \"Unable to load audit events\")}\n </p>\n </div>\n ) : auditLogs.length === 0 ? (\n <output className=\"block text-center py-12\">\n <p className=\"text-text-secondary\">\n {tr(\"auth.auditLog.noEvents\", \"No security events recorded\")}\n </p>\n </output>\n ) : (\n <>\n <div className=\"space-y-4\" aria-live=\"polite\" aria-busy={loading}>\n {auditLogs.map((event: AuditLogEntry_AuthAuditLogFragment) => (\n <div\n key={event.id}\n className=\"p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm\"\n >\n <div className=\"flex items-start gap-4\">\n <div\n className=\"flex size-10 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-secondary\"\n aria-hidden=\"true\"\n >\n {getEventMarker(event.action)}\n </div>\n <div className=\"flex-1\">\n <h3 className=\"text-lg font-medium text-text-primary mb-1\">\n {event.action.replace(/_/g, \" \")}\n </h3>\n <p className=\"text-sm text-text-secondary mb-2\">\n <time dateTime={event.createdAt}>\n {formatDateTime(event.createdAt)}\n </time>\n </p>\n <p className=\"text-sm text-text-secondary\">\n {event.description ??\n tr(\n \"auth.auditLog.defaultDescription\",\n \"Authentication event\",\n )}\n {event.ipAddress && ` • ${event.ipAddress}`}\n </p>\n </div>\n {event.action.includes(\"FAILURE\") && (\n <output className=\"px-3 py-1 rounded-full bg-status-error-bg-subtle text-status-error-text text-sm font-medium\">\n {tr(\"auth.auditLog.failed\", \"Failed\")}\n </output>\n )}\n </div>\n </div>\n ))}\n </div>\n\n {/* Pagination */}\n <nav\n aria-label={tr(\n \"auth.auditLog.paginationAriaLabel\",\n \"Audit log pagination\",\n )}\n className=\"mt-6 flex items-center justify-between\"\n >\n <p className=\"text-sm text-text-secondary\">\n {tr(\"auth.auditLog.page\", \"Page\")} {page}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => setPage((prev) => Math.max(1, prev - 1))}\n disabled={page === 1}\n aria-label={tr(\n \"auth.auditLog.previousPageAriaLabel\",\n \"Go to previous page\",\n )}\n className=\"px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"auth.auditLog.previous\", \"Previous\")}\n </button>\n <button\n type=\"button\"\n onClick={() => setPage((prev) => prev + 1)}\n disabled={!hasMore}\n aria-label={tr(\n \"auth.auditLog.nextPageAriaLabel\",\n \"Go to next page\",\n )}\n className=\"px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"auth.auditLog.next\", \"Next\")}\n </button>\n </div>\n </nav>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ import { jsx as r } from "react/jsx-runtime";
5
5
  import { clearShellSsoCookies as i } from "@burdenoff/fe-libs/shared/utils";
6
6
  import { getAuthBridge as a } from "@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient";
7
7
  //#region src/auth/pages/SSOClearPage.tsx
8
- var o = new Set(/* @__PURE__ */ "burdenoff.com,vibecontrols.com,fluidgrids.com,botlit.ai,bigconsole.com,gethealthybowl.com,headshotmarketing.com,timecampus.com,ggnomad.com,movethewheels.com,crewfoundry.com,planmagnet.com,eventfullymanaged.com,kadaikodi.com,assethandler.com,technospam.com,semanticfed.com,manufacturedops.com,theglobalfuel.com,housingvista.com,proserviceworld.com,mybodyshield.com,artistrybase.com,collabkin.com,brainyrich.com,theculturepost.com,comradecircle.com,cosmicintersection.com,govcitizenhub.com,intelwatchtower.com,ecoimpacthub.com,buildmyiq.com,fluidgrids.ai".split(","));
8
+ var o = new Set(/* @__PURE__ */ "burdenoff.com,vibecontrols.com,fluidgrids.com,botlit.ai,bigconsole.com,gethealthybowl.com,headshotmarketing.com,timecampus.com,ggnomad.com,movethewheels.com,crewfoundry.com,planmagnet.com,eventfullymanaged.com,kadaikodi.com,assethandler.com,technospam.com,semanticfed.com,manufacturedops.com,theglobalfuel.com,housingvista.com,proserviceworld.com,mybodyshield.com,artistrybase.com,collabkin.com,brainyrich.com,theculturepost.com,comradecircle.com,cosmicintersection.com,govcitizenhub.com,intelwatchtower.com,ecoimpacthub.com,buildmyiq.com,fluidgrids.ai,atheletebridge.com,adaptercloud.com,coolandlovely.com,subscriberbot.com,labsofscience.com,hookmovies.com".split(","));
9
9
  function s(e) {
10
10
  if (e.hostname === "localhost" || e.hostname === "127.0.0.1" || e.hostname.endsWith(".local.burdenoff.com")) return !0;
11
11
  if (e.protocol !== "https:") return !1;
@@ -1 +1 @@
1
- {"version":3,"file":"SSOClearPage.js","names":[],"sources":["../../../src/auth/pages/SSOClearPage.tsx"],"sourcesContent":["/**\n * SSOClearPage — Cross-Domain Logout at the Central Bridge Host\n *\n * Companion to SSODepositPage. When a user signs out on a product origin,\n * the product app cleared its own localStorage / cookies but the bridge\n * host (alphaapp/app.burdenoff.com) still held the deposited session in\n * first-party (unpartitioned) localStorage. A later visit to any product\n * would then bridge-host-relay that stale session back, effectively\n * undoing the logout.\n *\n * AuthProvider.logout() / logoutAll() on a non-bridge-host origin redirect\n * here via a top-level navigation:\n *\n * product.com (logout)\n * → app.burdenoff.com/auth/sso-clear?returnTo=<encoded-return-url>\n * — wipes bridge-host localStorage + bf-sso cookie\n * → <returnTo>\n *\n * URL: /auth/sso-clear\n */\n\nimport { useEffect } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { clearShellSsoCookies } from \"@burdenoff/fe-libs/shared/utils\";\nimport { clearLocalAuthState } from \"@burdenoff/fe-libs/shared/components\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\n\nconst ALLOWED_RETURN_DOMAINS = new Set([\n \"burdenoff.com\",\n \"vibecontrols.com\",\n \"fluidgrids.com\",\n \"botlit.ai\",\n \"bigconsole.com\",\n \"gethealthybowl.com\",\n \"headshotmarketing.com\",\n \"timecampus.com\",\n \"ggnomad.com\",\n \"movethewheels.com\",\n \"crewfoundry.com\",\n \"planmagnet.com\",\n \"eventfullymanaged.com\",\n \"kadaikodi.com\",\n \"assethandler.com\",\n \"technospam.com\",\n \"semanticfed.com\",\n \"manufacturedops.com\",\n \"theglobalfuel.com\",\n \"housingvista.com\",\n \"proserviceworld.com\",\n \"mybodyshield.com\",\n \"artistrybase.com\",\n \"collabkin.com\",\n \"brainyrich.com\",\n \"theculturepost.com\",\n \"comradecircle.com\",\n \"cosmicintersection.com\",\n \"govcitizenhub.com\",\n \"intelwatchtower.com\",\n \"ecoimpacthub.com\",\n \"buildmyiq.com\",\n \"fluidgrids.ai\",\n]);\n\nfunction isAllowedReturnUrl(url: URL): boolean {\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") return true;\n if (url.hostname.endsWith(\".local.burdenoff.com\")) return true;\n if (url.protocol !== \"https:\") return false;\n for (const domain of ALLOWED_RETURN_DOMAINS) {\n if (\n url.hostname === domain ||\n url.hostname === `app.${domain}` ||\n url.hostname === `alphaapp.${domain}` ||\n url.hostname === `www.${domain}`\n ) {\n return true;\n }\n }\n return false;\n}\n\nfunction resolveSafeReturn(returnTo: string | null): string {\n if (!returnTo) return \"/auth/login?reason=logout\";\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin === window.location.origin || isAllowedReturnUrl(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/auth/login?reason=logout\";\n}\n\nexport function SSOClearPage() {\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = resolveSafeReturn(searchParams.get(\"returnTo\"));\n\n try {\n clearLocalAuthState();\n clearShellSsoCookies();\n // Broadcast to any same-origin shells still listening so they drop the\n // session immediately too (best-effort — only fires on the bridge\n // host's origin, which is the only place where iframe storage isn't\n // partitioned cross-product).\n getAuthBridge()?.notifyLogoutAll();\n } catch {\n /* ignore — proceed with the redirect regardless */\n }\n\n window.location.replace(returnTo);\n }, [searchParams]);\n\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-bg-canvas\">\n <div className=\"animate-spin rounded-full size-8 border-2 border-action-primary-bg border-t-transparent\" />\n </div>\n );\n}\n"],"mappings":";;;;;;;AA2BA,IAAM,IAAyB,IAAI,IAAI,qkBAkCtC,CAAC;AAEF,SAAS,EAAmB,GAAmB;AAE7C,KADI,EAAI,aAAa,eAAe,EAAI,aAAa,eACjD,EAAI,SAAS,SAAS,uBAAuB,CAAE,QAAO;AAC1D,KAAI,EAAI,aAAa,SAAU,QAAO;AACtC,MAAK,IAAM,KAAU,EACnB,KACE,EAAI,aAAa,KACjB,EAAI,aAAa,OAAO,OACxB,EAAI,aAAa,YAAY,OAC7B,EAAI,aAAa,OAAO,IAExB,QAAO;AAGX,QAAO;;AAGT,SAAS,EAAkB,GAAiC;AAC1D,KAAI,CAAC,EAAU,QAAO;AACtB,KAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAU,OAAO,SAAS,OAAO;AACrD,MAAI,EAAI,WAAW,OAAO,SAAS,UAAU,EAAmB,EAAI,CAClE,QAAO,EAAI;SAEP;AAGR,QAAO;;AAGT,SAAgB,IAAe;CAC7B,IAAM,CAAC,KAAgB,GAAiB;AAoBxC,QAlBA,QAAgB;EACd,IAAM,IAAW,EAAkB,EAAa,IAAI,WAAW,CAAC;AAEhE,MAAI;AAOF,GANA,GAAqB,EACrB,GAAsB,EAKtB,GAAe,EAAE,iBAAiB;UAC5B;AAIR,SAAO,SAAS,QAAQ,EAAS;IAChC,CAAC,EAAa,CAAC,EAGhB,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD,EAAK,WAAU,2FAA4F,CAAA;EACvG,CAAA"}
1
+ {"version":3,"file":"SSOClearPage.js","names":[],"sources":["../../../src/auth/pages/SSOClearPage.tsx"],"sourcesContent":["/**\n * SSOClearPage — Cross-Domain Logout at the Central Bridge Host\n *\n * Companion to SSODepositPage. When a user signs out on a product origin,\n * the product app cleared its own localStorage / cookies but the bridge\n * host (alphaapp/app.burdenoff.com) still held the deposited session in\n * first-party (unpartitioned) localStorage. A later visit to any product\n * would then bridge-host-relay that stale session back, effectively\n * undoing the logout.\n *\n * AuthProvider.logout() / logoutAll() on a non-bridge-host origin redirect\n * here via a top-level navigation:\n *\n * product.com (logout)\n * → app.burdenoff.com/auth/sso-clear?returnTo=<encoded-return-url>\n * — wipes bridge-host localStorage + bf-sso cookie\n * → <returnTo>\n *\n * URL: /auth/sso-clear\n */\n\nimport { useEffect } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { clearShellSsoCookies } from \"@burdenoff/fe-libs/shared/utils\";\nimport { clearLocalAuthState } from \"@burdenoff/fe-libs/shared/components\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\n\nconst ALLOWED_RETURN_DOMAINS = new Set([\n \"burdenoff.com\",\n \"vibecontrols.com\",\n \"fluidgrids.com\",\n \"botlit.ai\",\n \"bigconsole.com\",\n \"gethealthybowl.com\",\n \"headshotmarketing.com\",\n \"timecampus.com\",\n \"ggnomad.com\",\n \"movethewheels.com\",\n \"crewfoundry.com\",\n \"planmagnet.com\",\n \"eventfullymanaged.com\",\n \"kadaikodi.com\",\n \"assethandler.com\",\n \"technospam.com\",\n \"semanticfed.com\",\n \"manufacturedops.com\",\n \"theglobalfuel.com\",\n \"housingvista.com\",\n \"proserviceworld.com\",\n \"mybodyshield.com\",\n \"artistrybase.com\",\n \"collabkin.com\",\n \"brainyrich.com\",\n \"theculturepost.com\",\n \"comradecircle.com\",\n \"cosmicintersection.com\",\n \"govcitizenhub.com\",\n \"intelwatchtower.com\",\n \"ecoimpacthub.com\",\n \"buildmyiq.com\",\n \"fluidgrids.ai\",\n \"atheletebridge.com\",\n \"adaptercloud.com\",\n \"coolandlovely.com\",\n \"subscriberbot.com\",\n \"labsofscience.com\",\n \"hookmovies.com\",\n]);\n\nfunction isAllowedReturnUrl(url: URL): boolean {\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") return true;\n if (url.hostname.endsWith(\".local.burdenoff.com\")) return true;\n if (url.protocol !== \"https:\") return false;\n for (const domain of ALLOWED_RETURN_DOMAINS) {\n if (\n url.hostname === domain ||\n url.hostname === `app.${domain}` ||\n url.hostname === `alphaapp.${domain}` ||\n url.hostname === `www.${domain}`\n ) {\n return true;\n }\n }\n return false;\n}\n\nfunction resolveSafeReturn(returnTo: string | null): string {\n if (!returnTo) return \"/auth/login?reason=logout\";\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin === window.location.origin || isAllowedReturnUrl(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/auth/login?reason=logout\";\n}\n\nexport function SSOClearPage() {\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = resolveSafeReturn(searchParams.get(\"returnTo\"));\n\n try {\n clearLocalAuthState();\n clearShellSsoCookies();\n // Broadcast to any same-origin shells still listening so they drop the\n // session immediately too (best-effort — only fires on the bridge\n // host's origin, which is the only place where iframe storage isn't\n // partitioned cross-product).\n getAuthBridge()?.notifyLogoutAll();\n } catch {\n /* ignore — proceed with the redirect regardless */\n }\n\n window.location.replace(returnTo);\n }, [searchParams]);\n\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-bg-canvas\">\n <div className=\"animate-spin rounded-full size-8 border-2 border-action-primary-bg border-t-transparent\" />\n </div>\n );\n}\n"],"mappings":";;;;;;;AA2BA,IAAM,IAAyB,IAAI,IAAI,8qBAwCtC,CAAC;AAEF,SAAS,EAAmB,GAAmB;AAE7C,KADI,EAAI,aAAa,eAAe,EAAI,aAAa,eACjD,EAAI,SAAS,SAAS,uBAAuB,CAAE,QAAO;AAC1D,KAAI,EAAI,aAAa,SAAU,QAAO;AACtC,MAAK,IAAM,KAAU,EACnB,KACE,EAAI,aAAa,KACjB,EAAI,aAAa,OAAO,OACxB,EAAI,aAAa,YAAY,OAC7B,EAAI,aAAa,OAAO,IAExB,QAAO;AAGX,QAAO;;AAGT,SAAS,EAAkB,GAAiC;AAC1D,KAAI,CAAC,EAAU,QAAO;AACtB,KAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAU,OAAO,SAAS,OAAO;AACrD,MAAI,EAAI,WAAW,OAAO,SAAS,UAAU,EAAmB,EAAI,CAClE,QAAO,EAAI;SAEP;AAGR,QAAO;;AAGT,SAAgB,IAAe;CAC7B,IAAM,CAAC,KAAgB,GAAiB;AAoBxC,QAlBA,QAAgB;EACd,IAAM,IAAW,EAAkB,EAAa,IAAI,WAAW,CAAC;AAEhE,MAAI;AAOF,GANA,GAAqB,EACrB,GAAsB,EAKtB,GAAe,EAAE,iBAAiB;UAC5B;AAIR,SAAO,SAAS,QAAQ,EAAS;IAChC,CAAC,EAAa,CAAC,EAGhB,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD,EAAK,WAAU,2FAA4F,CAAA;EACvG,CAAA"}
@@ -5,7 +5,7 @@ import { jsx as r } from "react/jsx-runtime";
5
5
  import { setSSoCookie as i } from "@burdenoff/fe-libs/shared/utils";
6
6
  import { getAuthBridge as a } from "@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient";
7
7
  //#region src/auth/pages/SSODepositPage.tsx
8
- var o = new Set(/* @__PURE__ */ "burdenoff.com,vibecontrols.com,fluidgrids.com,botlit.ai,bigconsole.com,gethealthybowl.com,headshotmarketing.com,timecampus.com,ggnomad.com,movethewheels.com,crewfoundry.com,planmagnet.com,eventfullymanaged.com,kadaikodi.com,assethandler.com,technospam.com,semanticfed.com,manufacturedops.com,theglobalfuel.com,housingvista.com,proserviceworld.com,mybodyshield.com,artistrybase.com,collabkin.com,brainyrich.com,theculturepost.com,comradecircle.com,cosmicintersection.com,govcitizenhub.com,intelwatchtower.com,ecoimpacthub.com,buildmyiq.com,fluidgrids.ai".split(","));
8
+ var o = new Set(/* @__PURE__ */ "burdenoff.com,vibecontrols.com,fluidgrids.com,botlit.ai,bigconsole.com,gethealthybowl.com,headshotmarketing.com,timecampus.com,ggnomad.com,movethewheels.com,crewfoundry.com,planmagnet.com,eventfullymanaged.com,kadaikodi.com,assethandler.com,technospam.com,semanticfed.com,manufacturedops.com,theglobalfuel.com,housingvista.com,proserviceworld.com,mybodyshield.com,artistrybase.com,collabkin.com,brainyrich.com,theculturepost.com,comradecircle.com,cosmicintersection.com,govcitizenhub.com,intelwatchtower.com,ecoimpacthub.com,buildmyiq.com,fluidgrids.ai,atheletebridge.com,adaptercloud.com,coolandlovely.com,subscriberbot.com,labsofscience.com,hookmovies.com".split(","));
9
9
  function s(e) {
10
10
  if (e.hostname === "localhost" || e.hostname === "127.0.0.1" || e.hostname.endsWith(".local.burdenoff.com")) return !0;
11
11
  if (e.protocol !== "https:") return !1;
@@ -1 +1 @@
1
- {"version":3,"file":"SSODepositPage.js","names":[],"sources":["../../../src/auth/pages/SSODepositPage.tsx"],"sourcesContent":["/**\n * SSODepositPage — Cross-Domain Token Deposit at the Central Bridge Host\n *\n * Loaded as a top-level navigation (NOT an iframe) on the bridge host\n * (alphaapp/app.burdenoff.com) right after a user signs in on any other\n * product origin. It receives the freshly-issued tokens in the URL fragment\n * (`#sso=<base64>` — fragments are never sent to servers) and stores them\n * in this origin's first-party (unpartitioned) localStorage so that:\n *\n * 1. A later top-level visit to the bridge host renders authenticated.\n * 2. Other product origins can discover the session via\n * `/auth/sso-relay?returnTo=...` against this host.\n *\n * Flow:\n * product.com/auth/login → user signs in\n * product.com → POST-success redirect to:\n * app.burdenoff.com/auth/sso-deposit?returnTo=<encoded-return-url>#sso=<base64>\n * → writes tokens here, sets bf-sso cookie\n * <returnTo> → user lands back at intended dashboard\n *\n * URL: /auth/sso-deposit\n */\n\nimport { useEffect } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { setSSoCookie } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useAuthStore } from \"../store/authStore\";\n\nconst ALLOWED_RETURN_DOMAINS = new Set([\n \"burdenoff.com\",\n \"vibecontrols.com\",\n \"fluidgrids.com\",\n \"botlit.ai\",\n \"bigconsole.com\",\n \"gethealthybowl.com\",\n \"headshotmarketing.com\",\n \"timecampus.com\",\n \"ggnomad.com\",\n \"movethewheels.com\",\n \"crewfoundry.com\",\n \"planmagnet.com\",\n \"eventfullymanaged.com\",\n \"kadaikodi.com\",\n \"assethandler.com\",\n \"technospam.com\",\n \"semanticfed.com\",\n \"manufacturedops.com\",\n \"theglobalfuel.com\",\n \"housingvista.com\",\n \"proserviceworld.com\",\n \"mybodyshield.com\",\n \"artistrybase.com\",\n \"collabkin.com\",\n \"brainyrich.com\",\n \"theculturepost.com\",\n \"comradecircle.com\",\n \"cosmicintersection.com\",\n \"govcitizenhub.com\",\n \"intelwatchtower.com\",\n \"ecoimpacthub.com\",\n \"buildmyiq.com\",\n \"fluidgrids.ai\",\n]);\n\nfunction isAllowedReturnUrl(url: URL): boolean {\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") return true;\n if (url.hostname.endsWith(\".local.burdenoff.com\")) return true;\n if (url.protocol !== \"https:\") return false;\n for (const domain of ALLOWED_RETURN_DOMAINS) {\n if (\n url.hostname === domain ||\n url.hostname === `app.${domain}` ||\n url.hostname === `alphaapp.${domain}` ||\n // Admin app shells (admin.burdenoff.com / alphaadmin.burdenoff.com) are\n // valid post-deposit return targets. Without these, a login that lands on\n // the bridge host falls back to \"/\" instead of returning to the admin app.\n url.hostname === `admin.${domain}` ||\n url.hostname === `alphaadmin.${domain}` ||\n url.hostname === `www.${domain}`\n ) {\n return true;\n }\n }\n return false;\n}\n\nfunction resolveSafeReturn(returnTo: string | null): string {\n if (!returnTo) return \"/\";\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin === window.location.origin || isAllowedReturnUrl(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/\";\n}\n\nexport function SSODepositPage() {\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = resolveSafeReturn(searchParams.get(\"returnTo\"));\n const hash = window.location.hash;\n\n if (!hash.startsWith(\"#sso=\")) {\n window.location.replace(returnTo);\n return;\n }\n\n try {\n const encoded = hash.slice(5);\n const payload = JSON.parse(atob(encoded)) as {\n profileId?: string;\n tokens?: string;\n user?: string;\n profiles?: string;\n };\n const { profileId, tokens, user, profiles } = payload;\n\n if (!profileId || !tokens) {\n window.location.replace(returnTo);\n return;\n }\n\n // First-party (unpartitioned) writes on the bridge host origin —\n // future top-level visits here see the session.\n localStorage.setItem(`bf-p-${profileId}-tokens`, tokens);\n if (user) {\n localStorage.setItem(`bf-p-${profileId}-user`, user);\n }\n if (profiles) {\n localStorage.setItem(\"bf-profiles\", profiles);\n }\n sessionStorage.setItem(\"bf-active-profile\", profileId);\n setSSoCookie(profileId);\n useAuthStore.getState().loadFromProfile(profileId);\n window.dispatchEvent(new Event(\"bf-profile-storage-change\"));\n\n // Notify the same-origin auth bridge so any other open shells on\n // this origin pick up the new session immediately.\n getAuthBridge()?.notifyLogin({\n user: user ? (JSON.parse(user) as Record<string, unknown>) : null,\n tokens: tokens ? (JSON.parse(tokens) as { accessToken: string }) : null,\n activeProfileId: profileId,\n profiles: profiles\n ? (JSON.parse(profiles) as Array<Record<string, unknown>>)\n : [],\n });\n\n window.location.replace(returnTo);\n } catch {\n window.location.replace(returnTo);\n }\n }, [searchParams]);\n\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-bg-canvas\">\n <div className=\"animate-spin rounded-full size-8 border-2 border-action-primary-bg border-t-transparent\" />\n </div>\n );\n}\n"],"mappings":";;;;;;;AA6BA,IAAM,IAAyB,IAAI,IAAI,qkBAkCtC,CAAC;AAEF,SAAS,EAAmB,GAAmB;AAE7C,KADI,EAAI,aAAa,eAAe,EAAI,aAAa,eACjD,EAAI,SAAS,SAAS,uBAAuB,CAAE,QAAO;AAC1D,KAAI,EAAI,aAAa,SAAU,QAAO;AACtC,MAAK,IAAM,KAAU,EACnB,KACE,EAAI,aAAa,KACjB,EAAI,aAAa,OAAO,OACxB,EAAI,aAAa,YAAY,OAI7B,EAAI,aAAa,SAAS,OAC1B,EAAI,aAAa,cAAc,OAC/B,EAAI,aAAa,OAAO,IAExB,QAAO;AAGX,QAAO;;AAGT,SAAS,EAAkB,GAAiC;AAC1D,KAAI,CAAC,EAAU,QAAO;AACtB,KAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAU,OAAO,SAAS,OAAO;AACrD,MAAI,EAAI,WAAW,OAAO,SAAS,UAAU,EAAmB,EAAI,CAClE,QAAO,EAAI;SAEP;AAGR,QAAO;;AAGT,SAAgB,IAAiB;CAC/B,IAAM,CAAC,KAAgB,GAAiB;AAyDxC,QAvDA,QAAgB;EACd,IAAM,IAAW,EAAkB,EAAa,IAAI,WAAW,CAAC,EAC1D,IAAO,OAAO,SAAS;AAE7B,MAAI,CAAC,EAAK,WAAW,QAAQ,EAAE;AAC7B,UAAO,SAAS,QAAQ,EAAS;AACjC;;AAGF,MAAI;GACF,IAAM,IAAU,EAAK,MAAM,EAAE,EAOvB,EAAE,cAAW,WAAQ,SAAM,gBANjB,KAAK,MAAM,KAAK,EAAQ,CAAC;AAQzC,OAAI,CAAC,KAAa,CAAC,GAAQ;AACzB,WAAO,SAAS,QAAQ,EAAS;AACjC;;AA4BF,GAvBA,aAAa,QAAQ,QAAQ,EAAU,UAAU,EAAO,EACpD,KACF,aAAa,QAAQ,QAAQ,EAAU,QAAQ,EAAK,EAElD,KACF,aAAa,QAAQ,eAAe,EAAS,EAE/C,eAAe,QAAQ,qBAAqB,EAAU,EACtD,EAAa,EAAU,EACvB,EAAa,UAAU,CAAC,gBAAgB,EAAU,EAClD,OAAO,cAAc,IAAI,MAAM,4BAA4B,CAAC,EAI5D,GAAe,EAAE,YAAY;IAC3B,MAAM,IAAQ,KAAK,MAAM,EAAK,GAA+B;IAC7D,QAAQ,IAAU,KAAK,MAAM,EAAO,GAA+B;IACnE,iBAAiB;IACjB,UAAU,IACL,KAAK,MAAM,EAAS,GACrB,EAAE;IACP,CAAC,EAEF,OAAO,SAAS,QAAQ,EAAS;UAC3B;AACN,UAAO,SAAS,QAAQ,EAAS;;IAElC,CAAC,EAAa,CAAC,EAGhB,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD,EAAK,WAAU,2FAA4F,CAAA;EACvG,CAAA"}
1
+ {"version":3,"file":"SSODepositPage.js","names":[],"sources":["../../../src/auth/pages/SSODepositPage.tsx"],"sourcesContent":["/**\n * SSODepositPage — Cross-Domain Token Deposit at the Central Bridge Host\n *\n * Loaded as a top-level navigation (NOT an iframe) on the bridge host\n * (alphaapp/app.burdenoff.com) right after a user signs in on any other\n * product origin. It receives the freshly-issued tokens in the URL fragment\n * (`#sso=<base64>` — fragments are never sent to servers) and stores them\n * in this origin's first-party (unpartitioned) localStorage so that:\n *\n * 1. A later top-level visit to the bridge host renders authenticated.\n * 2. Other product origins can discover the session via\n * `/auth/sso-relay?returnTo=...` against this host.\n *\n * Flow:\n * product.com/auth/login → user signs in\n * product.com → POST-success redirect to:\n * app.burdenoff.com/auth/sso-deposit?returnTo=<encoded-return-url>#sso=<base64>\n * → writes tokens here, sets bf-sso cookie\n * <returnTo> → user lands back at intended dashboard\n *\n * URL: /auth/sso-deposit\n */\n\nimport { useEffect } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { setSSoCookie } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useAuthStore } from \"../store/authStore\";\n\nconst ALLOWED_RETURN_DOMAINS = new Set([\n \"burdenoff.com\",\n \"vibecontrols.com\",\n \"fluidgrids.com\",\n \"botlit.ai\",\n \"bigconsole.com\",\n \"gethealthybowl.com\",\n \"headshotmarketing.com\",\n \"timecampus.com\",\n \"ggnomad.com\",\n \"movethewheels.com\",\n \"crewfoundry.com\",\n \"planmagnet.com\",\n \"eventfullymanaged.com\",\n \"kadaikodi.com\",\n \"assethandler.com\",\n \"technospam.com\",\n \"semanticfed.com\",\n \"manufacturedops.com\",\n \"theglobalfuel.com\",\n \"housingvista.com\",\n \"proserviceworld.com\",\n \"mybodyshield.com\",\n \"artistrybase.com\",\n \"collabkin.com\",\n \"brainyrich.com\",\n \"theculturepost.com\",\n \"comradecircle.com\",\n \"cosmicintersection.com\",\n \"govcitizenhub.com\",\n \"intelwatchtower.com\",\n \"ecoimpacthub.com\",\n \"buildmyiq.com\",\n \"fluidgrids.ai\",\n \"atheletebridge.com\",\n \"adaptercloud.com\",\n \"coolandlovely.com\",\n \"subscriberbot.com\",\n \"labsofscience.com\",\n \"hookmovies.com\",\n]);\n\nfunction isAllowedReturnUrl(url: URL): boolean {\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") return true;\n if (url.hostname.endsWith(\".local.burdenoff.com\")) return true;\n if (url.protocol !== \"https:\") return false;\n for (const domain of ALLOWED_RETURN_DOMAINS) {\n if (\n url.hostname === domain ||\n url.hostname === `app.${domain}` ||\n url.hostname === `alphaapp.${domain}` ||\n // Admin app shells (admin.burdenoff.com / alphaadmin.burdenoff.com) are\n // valid post-deposit return targets. Without these, a login that lands on\n // the bridge host falls back to \"/\" instead of returning to the admin app.\n url.hostname === `admin.${domain}` ||\n url.hostname === `alphaadmin.${domain}` ||\n url.hostname === `www.${domain}`\n ) {\n return true;\n }\n }\n return false;\n}\n\nfunction resolveSafeReturn(returnTo: string | null): string {\n if (!returnTo) return \"/\";\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin === window.location.origin || isAllowedReturnUrl(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/\";\n}\n\nexport function SSODepositPage() {\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = resolveSafeReturn(searchParams.get(\"returnTo\"));\n const hash = window.location.hash;\n\n if (!hash.startsWith(\"#sso=\")) {\n window.location.replace(returnTo);\n return;\n }\n\n try {\n const encoded = hash.slice(5);\n const payload = JSON.parse(atob(encoded)) as {\n profileId?: string;\n tokens?: string;\n user?: string;\n profiles?: string;\n };\n const { profileId, tokens, user, profiles } = payload;\n\n if (!profileId || !tokens) {\n window.location.replace(returnTo);\n return;\n }\n\n // First-party (unpartitioned) writes on the bridge host origin —\n // future top-level visits here see the session.\n localStorage.setItem(`bf-p-${profileId}-tokens`, tokens);\n if (user) {\n localStorage.setItem(`bf-p-${profileId}-user`, user);\n }\n if (profiles) {\n localStorage.setItem(\"bf-profiles\", profiles);\n }\n sessionStorage.setItem(\"bf-active-profile\", profileId);\n setSSoCookie(profileId);\n useAuthStore.getState().loadFromProfile(profileId);\n window.dispatchEvent(new Event(\"bf-profile-storage-change\"));\n\n // Notify the same-origin auth bridge so any other open shells on\n // this origin pick up the new session immediately.\n getAuthBridge()?.notifyLogin({\n user: user ? (JSON.parse(user) as Record<string, unknown>) : null,\n tokens: tokens ? (JSON.parse(tokens) as { accessToken: string }) : null,\n activeProfileId: profileId,\n profiles: profiles\n ? (JSON.parse(profiles) as Array<Record<string, unknown>>)\n : [],\n });\n\n window.location.replace(returnTo);\n } catch {\n window.location.replace(returnTo);\n }\n }, [searchParams]);\n\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-bg-canvas\">\n <div className=\"animate-spin rounded-full size-8 border-2 border-action-primary-bg border-t-transparent\" />\n </div>\n );\n}\n"],"mappings":";;;;;;;AA6BA,IAAM,IAAyB,IAAI,IAAI,8qBAwCtC,CAAC;AAEF,SAAS,EAAmB,GAAmB;AAE7C,KADI,EAAI,aAAa,eAAe,EAAI,aAAa,eACjD,EAAI,SAAS,SAAS,uBAAuB,CAAE,QAAO;AAC1D,KAAI,EAAI,aAAa,SAAU,QAAO;AACtC,MAAK,IAAM,KAAU,EACnB,KACE,EAAI,aAAa,KACjB,EAAI,aAAa,OAAO,OACxB,EAAI,aAAa,YAAY,OAI7B,EAAI,aAAa,SAAS,OAC1B,EAAI,aAAa,cAAc,OAC/B,EAAI,aAAa,OAAO,IAExB,QAAO;AAGX,QAAO;;AAGT,SAAS,EAAkB,GAAiC;AAC1D,KAAI,CAAC,EAAU,QAAO;AACtB,KAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAU,OAAO,SAAS,OAAO;AACrD,MAAI,EAAI,WAAW,OAAO,SAAS,UAAU,EAAmB,EAAI,CAClE,QAAO,EAAI;SAEP;AAGR,QAAO;;AAGT,SAAgB,IAAiB;CAC/B,IAAM,CAAC,KAAgB,GAAiB;AAyDxC,QAvDA,QAAgB;EACd,IAAM,IAAW,EAAkB,EAAa,IAAI,WAAW,CAAC,EAC1D,IAAO,OAAO,SAAS;AAE7B,MAAI,CAAC,EAAK,WAAW,QAAQ,EAAE;AAC7B,UAAO,SAAS,QAAQ,EAAS;AACjC;;AAGF,MAAI;GACF,IAAM,IAAU,EAAK,MAAM,EAAE,EAOvB,EAAE,cAAW,WAAQ,SAAM,gBANjB,KAAK,MAAM,KAAK,EAAQ,CAAC;AAQzC,OAAI,CAAC,KAAa,CAAC,GAAQ;AACzB,WAAO,SAAS,QAAQ,EAAS;AACjC;;AA4BF,GAvBA,aAAa,QAAQ,QAAQ,EAAU,UAAU,EAAO,EACpD,KACF,aAAa,QAAQ,QAAQ,EAAU,QAAQ,EAAK,EAElD,KACF,aAAa,QAAQ,eAAe,EAAS,EAE/C,eAAe,QAAQ,qBAAqB,EAAU,EACtD,EAAa,EAAU,EACvB,EAAa,UAAU,CAAC,gBAAgB,EAAU,EAClD,OAAO,cAAc,IAAI,MAAM,4BAA4B,CAAC,EAI5D,GAAe,EAAE,YAAY;IAC3B,MAAM,IAAQ,KAAK,MAAM,EAAK,GAA+B;IAC7D,QAAQ,IAAU,KAAK,MAAM,EAAO,GAA+B;IACnE,iBAAiB;IACjB,UAAU,IACL,KAAK,MAAM,EAAS,GACrB,EAAE;IACP,CAAC,EAEF,OAAO,SAAS,QAAQ,EAAS;UAC3B;AACN,UAAO,SAAS,QAAQ,EAAS;;IAElC,CAAC,EAAa,CAAC,EAGhB,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD,EAAK,WAAU,2FAA4F,CAAA;EACvG,CAAA"}
@@ -2,7 +2,7 @@ import { useEffect as e } from "react";
2
2
  import { useSearchParams as t } from "react-router-dom";
3
3
  import { jsx as n } from "react/jsx-runtime";
4
4
  //#region src/auth/pages/SSORelayPage.tsx
5
- var r = new Set(/* @__PURE__ */ "burdenoff.com,vibecontrols.com,fluidgrids.com,botlit.ai,bigconsole.com,gethealthybowl.com,headshotmarketing.com,timecampus.com,ggnomad.com,movethewheels.com,crewfoundry.com,planmagnet.com,eventfullymanaged.com,kadaikodi.com,assethandler.com,technospam.com,semanticfed.com,manufacturedops.com,theglobalfuel.com,housingvista.com,proserviceworld.com,mybodyshield.com,artistrybase.com,collabkin.com,brainyrich.com,theculturepost.com,comradecircle.com,cosmicintersection.com,govcitizenhub.com,intelwatchtower.com,ecoimpacthub.com,buildmyiq.com,fluidgrids.ai".split(",")), i = "relayChain", a = "sso_failed", o = new Set(["localhost", "127.0.0.1"]), s = {
5
+ var r = new Set(/* @__PURE__ */ "burdenoff.com,vibecontrols.com,fluidgrids.com,botlit.ai,bigconsole.com,gethealthybowl.com,headshotmarketing.com,timecampus.com,ggnomad.com,movethewheels.com,crewfoundry.com,planmagnet.com,eventfullymanaged.com,kadaikodi.com,assethandler.com,technospam.com,semanticfed.com,manufacturedops.com,theglobalfuel.com,housingvista.com,proserviceworld.com,mybodyshield.com,artistrybase.com,collabkin.com,brainyrich.com,theculturepost.com,comradecircle.com,cosmicintersection.com,govcitizenhub.com,intelwatchtower.com,ecoimpacthub.com,buildmyiq.com,fluidgrids.ai,atheletebridge.com,adaptercloud.com,coolandlovely.com,subscriberbot.com,labsofscience.com,hookmovies.com".split(",")), i = "relayChain", a = "sso_failed", o = new Set(["localhost", "127.0.0.1"]), s = {
6
6
  "bf-sso": /(?:^|;\s*)bf-sso=([^;]*)/,
7
7
  "bf-sso-profile": /(?:^|;\s*)bf-sso-profile=([^;]*)/
8
8
  };
@@ -1 +1 @@
1
- {"version":3,"file":"SSORelayPage.js","names":[],"sources":["../../../src/auth/pages/SSORelayPage.tsx"],"sourcesContent":["/**\n * SSORelayPage — Cross-domain SSO token relay.\n *\n * This page is loaded on the ORIGIN where the user is logged in.\n * It reads tokens from localStorage and redirects to the TARGET domain's\n * /auth/sso-callback with the tokens in a URL fragment (not query param\n * for security — fragments are not sent to servers).\n *\n * Flow:\n * 1. User visits productA.com → not logged in → detects SSO cookie\n * 2. Redirect to productB.com/auth/sso-relay?returnTo=productA.com/dashboard\n * (productB is where the user IS logged in — identified by cookie)\n * 3. This page reads tokens from productB's localStorage\n * 4. Redirects to productA.com/auth/sso-callback#token=<base64-encoded-tokens>\n * 5. SSORelayCallbackPage on productA decodes and stores tokens, redirects to /dashboard\n */\n\nimport { useEffect } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\n\nconst ALLOWED_RELAY_DOMAINS = new Set([\n \"burdenoff.com\",\n \"vibecontrols.com\",\n \"fluidgrids.com\",\n \"botlit.ai\",\n \"bigconsole.com\",\n \"gethealthybowl.com\",\n \"headshotmarketing.com\",\n \"timecampus.com\",\n \"ggnomad.com\",\n \"movethewheels.com\",\n \"crewfoundry.com\",\n \"planmagnet.com\",\n \"eventfullymanaged.com\",\n \"kadaikodi.com\",\n \"assethandler.com\",\n \"technospam.com\",\n \"semanticfed.com\",\n \"manufacturedops.com\",\n \"theglobalfuel.com\",\n \"housingvista.com\",\n \"proserviceworld.com\",\n \"mybodyshield.com\",\n \"artistrybase.com\",\n \"collabkin.com\",\n \"brainyrich.com\",\n \"theculturepost.com\",\n \"comradecircle.com\",\n \"cosmicintersection.com\",\n \"govcitizenhub.com\",\n \"intelwatchtower.com\",\n \"ecoimpacthub.com\",\n \"buildmyiq.com\",\n \"fluidgrids.ai\",\n]);\n\nconst RELAY_CHAIN_PARAM = \"relayChain\";\nconst RELAY_FAILED_PARAM = \"sso_failed\";\nconst LOCAL_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\"]);\n\nconst SSO_COOKIE_PATTERNS = {\n \"bf-sso\": /(?:^|;\\s*)bf-sso=([^;]*)/,\n \"bf-sso-profile\": /(?:^|;\\s*)bf-sso-profile=([^;]*)/,\n} as const;\n\nfunction isAllowedRelayTarget(url: URL): boolean {\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") {\n return true;\n }\n if (url.hostname.endsWith(\".local.burdenoff.com\")) {\n return true;\n }\n if (url.protocol !== \"https:\") {\n return false;\n }\n for (const domain of ALLOWED_RELAY_DOMAINS) {\n if (\n url.hostname === domain ||\n url.hostname === `app.${domain}` ||\n url.hostname === `alphaapp.${domain}` ||\n // Admin app shells (admin.burdenoff.com / alphaadmin.burdenoff.com) are\n // first-party SSO relay targets. Without these the relay-back to the\n // admin shell fails isAllowedRelayTarget() and SSORelayPage falls through\n // to window.location.replace(\"/\"), stranding the user on app.burdenoff.com.\n url.hostname === `admin.${domain}` ||\n url.hostname === `alphaadmin.${domain}` ||\n url.hostname === `www.${domain}`\n ) {\n return true;\n }\n }\n return false;\n}\n\nfunction clearRelayCookies(): void {\n try {\n const hostname = window.location.hostname;\n const domain = hostname.endsWith(\".local.burdenoff.com\")\n ? \".local.burdenoff.com\"\n : undefined;\n for (const cookieName of [\"bf-sso\", \"bf-sso-profile\"] as const) {\n document.cookie = `${cookieName}=; path=/; max-age=0`;\n if (domain) {\n document.cookie = `${cookieName}=; path=/; domain=${domain}; max-age=0`;\n }\n if (hostname && !LOCAL_HOSTNAMES.has(hostname)) {\n document.cookie = `${cookieName}=; path=/; domain=${hostname}; max-age=0`;\n }\n }\n } catch {\n /* best effort */\n }\n}\n\nfunction getRelayChain(): string[] {\n try {\n return (\n new URLSearchParams(window.location.search).get(RELAY_CHAIN_PARAM) || \"\"\n )\n .split(\"|\")\n .filter((origin) => origin.length > 0);\n } catch {\n return [];\n }\n}\n\nfunction getFailedReturnTo(returnTo: string): string {\n try {\n const targetUrl = new URL(returnTo, window.location.origin);\n if (\n targetUrl.origin !== window.location.origin &&\n !isAllowedRelayTarget(targetUrl)\n ) {\n return \"/auth/login\";\n }\n targetUrl.searchParams.set(RELAY_FAILED_PARAM, \"1\");\n return targetUrl.href;\n } catch {\n return \"/auth/login?sso_failed=1\";\n }\n}\n\nfunction readSsoCookieProfileId(): string | null {\n return readSsoCookieData()?.id ?? null;\n}\n\nfunction readSsoCookieData(): {\n id: string | null;\n origin: string | null;\n} | null {\n try {\n for (const cookieName of [\"bf-sso\", \"bf-sso-profile\"] as const) {\n const match = document.cookie.match(SSO_COOKIE_PATTERNS[cookieName]);\n if (!match) continue;\n\n const parsed = JSON.parse(decodeURIComponent(match[1])) as\n | string\n | { id?: unknown; origin?: unknown };\n if (typeof parsed === \"string\") return { id: parsed, origin: null };\n if (typeof parsed.id === \"string\" && parsed.id.length > 0) {\n return {\n id: parsed.id,\n origin: typeof parsed.origin === \"string\" ? parsed.origin : null,\n };\n }\n }\n } catch {\n return null;\n }\n return null;\n}\n\nfunction redirectToCookieRelayOrigin(returnTo: string): boolean {\n try {\n const ssoCookie = readSsoCookieData();\n if (!ssoCookie?.origin) return false;\n\n const relayUrl = new URL(ssoCookie.origin);\n if (!isAllowedRelayTarget(relayUrl)) return false;\n if (relayUrl.origin === window.location.origin) return false;\n\n const relayChain = getRelayChain();\n if (relayChain.includes(relayUrl.origin) || relayChain.length >= 3) {\n return false;\n }\n\n const nextRelayUrl = new URL(\"/auth/sso-relay\", relayUrl.origin);\n nextRelayUrl.searchParams.set(\"returnTo\", returnTo);\n nextRelayUrl.searchParams.set(\n RELAY_CHAIN_PARAM,\n Array.from(new Set([...relayChain, window.location.origin])).join(\"|\"),\n );\n\n window.location.replace(nextRelayUrl.href);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction readProfilesRaw(): string | null {\n return localStorage.getItem(\"bf-profiles\");\n}\n\nfunction readProfileIds(): string[] {\n try {\n const profilesRaw = readProfilesRaw();\n const profiles = profilesRaw\n ? (JSON.parse(profilesRaw) as Array<{ id?: unknown }>)\n : [];\n return profiles\n .map((profile) => profile.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0);\n } catch {\n return [];\n }\n}\n\nfunction resolveRelayProfileId(): string | null {\n const candidates = [\n readSsoCookieProfileId(),\n sessionStorage.getItem(\"bf-active-profile\"),\n ...readProfileIds(),\n ].filter((id): id is string => typeof id === \"string\" && id.length > 0);\n\n for (const profileId of Array.from(new Set(candidates))) {\n const tokensRaw = localStorage.getItem(`bf-p-${profileId}-tokens`);\n const userRaw = localStorage.getItem(`bf-p-${profileId}-user`);\n if (tokensRaw && userRaw) {\n sessionStorage.setItem(\"bf-active-profile\", profileId);\n return profileId;\n }\n }\n\n return null;\n}\n\nexport function SSORelayPage() {\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = searchParams.get(\"returnTo\");\n if (!returnTo) {\n // No return URL — just show a blank page\n return;\n }\n\n // Read tokens from localStorage (this origin has them). Prefer the shared\n // SSO cookie profile because a relay often runs in a new tab with empty\n // sessionStorage and stale historical profiles in localStorage.\n const profileId = resolveRelayProfileId();\n if (!profileId) {\n if (redirectToCookieRelayOrigin(returnTo)) return;\n clearRelayCookies();\n window.location.replace(getFailedReturnTo(returnTo));\n return;\n }\n\n const tokensRaw = localStorage.getItem(`bf-p-${profileId}-tokens`);\n const userRaw = localStorage.getItem(`bf-p-${profileId}-user`);\n const profilesRaw = readProfilesRaw();\n\n if (!tokensRaw || !userRaw) {\n // No tokens — redirect back without them\n if (redirectToCookieRelayOrigin(returnTo)) return;\n clearRelayCookies();\n window.location.replace(getFailedReturnTo(returnTo));\n return;\n }\n\n // Encode the auth state as a base64 URL fragment\n const payload = {\n profileId,\n tokens: tokensRaw,\n user: userRaw,\n profiles: profilesRaw,\n };\n const encoded = btoa(JSON.stringify(payload));\n\n // Build the callback URL on the target domain\n // Parse returnTo to get the target origin\n let callbackUrl: string;\n try {\n const targetUrl = new URL(returnTo);\n if (!isAllowedRelayTarget(targetUrl)) {\n window.location.replace(\"/\");\n return;\n }\n const returnPath = targetUrl.pathname + targetUrl.search;\n callbackUrl = `${targetUrl.origin}/auth/sso-callback?returnTo=${encodeURIComponent(returnPath)}#sso=${encoded}`;\n } catch {\n // returnTo is a relative path — can't do cross-domain relay\n window.location.replace(returnTo);\n return;\n }\n\n // Redirect to the callback\n window.location.replace(callbackUrl);\n }, [searchParams]);\n\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-bg-canvas\">\n <div className=\"animate-spin rounded-full size-8 border-2 border-action-primary-bg border-t-transparent\" />\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SSORelayPage.js","names":[],"sources":["../../../src/auth/pages/SSORelayPage.tsx"],"sourcesContent":["/**\n * SSORelayPage — Cross-domain SSO token relay.\n *\n * This page is loaded on the ORIGIN where the user is logged in.\n * It reads tokens from localStorage and redirects to the TARGET domain's\n * /auth/sso-callback with the tokens in a URL fragment (not query param\n * for security — fragments are not sent to servers).\n *\n * Flow:\n * 1. User visits productA.com → not logged in → detects SSO cookie\n * 2. Redirect to productB.com/auth/sso-relay?returnTo=productA.com/dashboard\n * (productB is where the user IS logged in — identified by cookie)\n * 3. This page reads tokens from productB's localStorage\n * 4. Redirects to productA.com/auth/sso-callback#token=<base64-encoded-tokens>\n * 5. SSORelayCallbackPage on productA decodes and stores tokens, redirects to /dashboard\n */\n\nimport { useEffect } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\n\nconst ALLOWED_RELAY_DOMAINS = new Set([\n \"burdenoff.com\",\n \"vibecontrols.com\",\n \"fluidgrids.com\",\n \"botlit.ai\",\n \"bigconsole.com\",\n \"gethealthybowl.com\",\n \"headshotmarketing.com\",\n \"timecampus.com\",\n \"ggnomad.com\",\n \"movethewheels.com\",\n \"crewfoundry.com\",\n \"planmagnet.com\",\n \"eventfullymanaged.com\",\n \"kadaikodi.com\",\n \"assethandler.com\",\n \"technospam.com\",\n \"semanticfed.com\",\n \"manufacturedops.com\",\n \"theglobalfuel.com\",\n \"housingvista.com\",\n \"proserviceworld.com\",\n \"mybodyshield.com\",\n \"artistrybase.com\",\n \"collabkin.com\",\n \"brainyrich.com\",\n \"theculturepost.com\",\n \"comradecircle.com\",\n \"cosmicintersection.com\",\n \"govcitizenhub.com\",\n \"intelwatchtower.com\",\n \"ecoimpacthub.com\",\n \"buildmyiq.com\",\n \"fluidgrids.ai\",\n \"atheletebridge.com\",\n \"adaptercloud.com\",\n \"coolandlovely.com\",\n \"subscriberbot.com\",\n \"labsofscience.com\",\n \"hookmovies.com\",\n]);\n\nconst RELAY_CHAIN_PARAM = \"relayChain\";\nconst RELAY_FAILED_PARAM = \"sso_failed\";\nconst LOCAL_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\"]);\n\nconst SSO_COOKIE_PATTERNS = {\n \"bf-sso\": /(?:^|;\\s*)bf-sso=([^;]*)/,\n \"bf-sso-profile\": /(?:^|;\\s*)bf-sso-profile=([^;]*)/,\n} as const;\n\nfunction isAllowedRelayTarget(url: URL): boolean {\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") {\n return true;\n }\n if (url.hostname.endsWith(\".local.burdenoff.com\")) {\n return true;\n }\n if (url.protocol !== \"https:\") {\n return false;\n }\n for (const domain of ALLOWED_RELAY_DOMAINS) {\n if (\n url.hostname === domain ||\n url.hostname === `app.${domain}` ||\n url.hostname === `alphaapp.${domain}` ||\n // Admin app shells (admin.burdenoff.com / alphaadmin.burdenoff.com) are\n // first-party SSO relay targets. Without these the relay-back to the\n // admin shell fails isAllowedRelayTarget() and SSORelayPage falls through\n // to window.location.replace(\"/\"), stranding the user on app.burdenoff.com.\n url.hostname === `admin.${domain}` ||\n url.hostname === `alphaadmin.${domain}` ||\n url.hostname === `www.${domain}`\n ) {\n return true;\n }\n }\n return false;\n}\n\nfunction clearRelayCookies(): void {\n try {\n const hostname = window.location.hostname;\n const domain = hostname.endsWith(\".local.burdenoff.com\")\n ? \".local.burdenoff.com\"\n : undefined;\n for (const cookieName of [\"bf-sso\", \"bf-sso-profile\"] as const) {\n document.cookie = `${cookieName}=; path=/; max-age=0`;\n if (domain) {\n document.cookie = `${cookieName}=; path=/; domain=${domain}; max-age=0`;\n }\n if (hostname && !LOCAL_HOSTNAMES.has(hostname)) {\n document.cookie = `${cookieName}=; path=/; domain=${hostname}; max-age=0`;\n }\n }\n } catch {\n /* best effort */\n }\n}\n\nfunction getRelayChain(): string[] {\n try {\n return (\n new URLSearchParams(window.location.search).get(RELAY_CHAIN_PARAM) || \"\"\n )\n .split(\"|\")\n .filter((origin) => origin.length > 0);\n } catch {\n return [];\n }\n}\n\nfunction getFailedReturnTo(returnTo: string): string {\n try {\n const targetUrl = new URL(returnTo, window.location.origin);\n if (\n targetUrl.origin !== window.location.origin &&\n !isAllowedRelayTarget(targetUrl)\n ) {\n return \"/auth/login\";\n }\n targetUrl.searchParams.set(RELAY_FAILED_PARAM, \"1\");\n return targetUrl.href;\n } catch {\n return \"/auth/login?sso_failed=1\";\n }\n}\n\nfunction readSsoCookieProfileId(): string | null {\n return readSsoCookieData()?.id ?? null;\n}\n\nfunction readSsoCookieData(): {\n id: string | null;\n origin: string | null;\n} | null {\n try {\n for (const cookieName of [\"bf-sso\", \"bf-sso-profile\"] as const) {\n const match = document.cookie.match(SSO_COOKIE_PATTERNS[cookieName]);\n if (!match) continue;\n\n const parsed = JSON.parse(decodeURIComponent(match[1])) as\n | string\n | { id?: unknown; origin?: unknown };\n if (typeof parsed === \"string\") return { id: parsed, origin: null };\n if (typeof parsed.id === \"string\" && parsed.id.length > 0) {\n return {\n id: parsed.id,\n origin: typeof parsed.origin === \"string\" ? parsed.origin : null,\n };\n }\n }\n } catch {\n return null;\n }\n return null;\n}\n\nfunction redirectToCookieRelayOrigin(returnTo: string): boolean {\n try {\n const ssoCookie = readSsoCookieData();\n if (!ssoCookie?.origin) return false;\n\n const relayUrl = new URL(ssoCookie.origin);\n if (!isAllowedRelayTarget(relayUrl)) return false;\n if (relayUrl.origin === window.location.origin) return false;\n\n const relayChain = getRelayChain();\n if (relayChain.includes(relayUrl.origin) || relayChain.length >= 3) {\n return false;\n }\n\n const nextRelayUrl = new URL(\"/auth/sso-relay\", relayUrl.origin);\n nextRelayUrl.searchParams.set(\"returnTo\", returnTo);\n nextRelayUrl.searchParams.set(\n RELAY_CHAIN_PARAM,\n Array.from(new Set([...relayChain, window.location.origin])).join(\"|\"),\n );\n\n window.location.replace(nextRelayUrl.href);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction readProfilesRaw(): string | null {\n return localStorage.getItem(\"bf-profiles\");\n}\n\nfunction readProfileIds(): string[] {\n try {\n const profilesRaw = readProfilesRaw();\n const profiles = profilesRaw\n ? (JSON.parse(profilesRaw) as Array<{ id?: unknown }>)\n : [];\n return profiles\n .map((profile) => profile.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0);\n } catch {\n return [];\n }\n}\n\nfunction resolveRelayProfileId(): string | null {\n const candidates = [\n readSsoCookieProfileId(),\n sessionStorage.getItem(\"bf-active-profile\"),\n ...readProfileIds(),\n ].filter((id): id is string => typeof id === \"string\" && id.length > 0);\n\n for (const profileId of Array.from(new Set(candidates))) {\n const tokensRaw = localStorage.getItem(`bf-p-${profileId}-tokens`);\n const userRaw = localStorage.getItem(`bf-p-${profileId}-user`);\n if (tokensRaw && userRaw) {\n sessionStorage.setItem(\"bf-active-profile\", profileId);\n return profileId;\n }\n }\n\n return null;\n}\n\nexport function SSORelayPage() {\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = searchParams.get(\"returnTo\");\n if (!returnTo) {\n // No return URL — just show a blank page\n return;\n }\n\n // Read tokens from localStorage (this origin has them). Prefer the shared\n // SSO cookie profile because a relay often runs in a new tab with empty\n // sessionStorage and stale historical profiles in localStorage.\n const profileId = resolveRelayProfileId();\n if (!profileId) {\n if (redirectToCookieRelayOrigin(returnTo)) return;\n clearRelayCookies();\n window.location.replace(getFailedReturnTo(returnTo));\n return;\n }\n\n const tokensRaw = localStorage.getItem(`bf-p-${profileId}-tokens`);\n const userRaw = localStorage.getItem(`bf-p-${profileId}-user`);\n const profilesRaw = readProfilesRaw();\n\n if (!tokensRaw || !userRaw) {\n // No tokens — redirect back without them\n if (redirectToCookieRelayOrigin(returnTo)) return;\n clearRelayCookies();\n window.location.replace(getFailedReturnTo(returnTo));\n return;\n }\n\n // Encode the auth state as a base64 URL fragment\n const payload = {\n profileId,\n tokens: tokensRaw,\n user: userRaw,\n profiles: profilesRaw,\n };\n const encoded = btoa(JSON.stringify(payload));\n\n // Build the callback URL on the target domain\n // Parse returnTo to get the target origin\n let callbackUrl: string;\n try {\n const targetUrl = new URL(returnTo);\n if (!isAllowedRelayTarget(targetUrl)) {\n window.location.replace(\"/\");\n return;\n }\n const returnPath = targetUrl.pathname + targetUrl.search;\n callbackUrl = `${targetUrl.origin}/auth/sso-callback?returnTo=${encodeURIComponent(returnPath)}#sso=${encoded}`;\n } catch {\n // returnTo is a relative path — can't do cross-domain relay\n window.location.replace(returnTo);\n return;\n }\n\n // Redirect to the callback\n window.location.replace(callbackUrl);\n }, [searchParams]);\n\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-bg-canvas\">\n <div className=\"animate-spin rounded-full size-8 border-2 border-action-primary-bg border-t-transparent\" />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -323,7 +323,7 @@ function v({ enableAdminRoutes: v = !1 }) {
323
323
  className: "flex items-start gap-4",
324
324
  children: [
325
325
  /* @__PURE__ */ u("div", {
326
- className: `size-12 rounded-xl ${e.bgColor} flex items-center justify-center flex-shrink-0 transition-transform group-hover:scale-110`,
326
+ className: `size-12 rounded-xl ${e.bgColor} flex items-center justify-center flex-shrink-0 transition-transform`,
327
327
  children: /* @__PURE__ */ u(e.icon, { className: `size-6 ${e.color}` })
328
328
  }),
329
329
  /* @__PURE__ */ d("div", {
@@ -358,7 +358,7 @@ function v({ enableAdminRoutes: v = !1 }) {
358
358
  className: "flex items-start gap-4",
359
359
  children: [
360
360
  /* @__PURE__ */ u("div", {
361
- className: `size-12 rounded-xl ${e.bgColor} flex items-center justify-center flex-shrink-0 transition-transform group-hover:scale-110`,
361
+ className: `size-12 rounded-xl ${e.bgColor} flex items-center justify-center flex-shrink-0 transition-transform`,
362
362
  children: /* @__PURE__ */ u(e.icon, { className: `size-6 ${e.color}` })
363
363
  }),
364
364
  /* @__PURE__ */ d("div", {