@eminuckan/spine 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/SECURITY.md +5 -15
  3. package/dist/access/index.mjs +2 -0
  4. package/dist/{access-CGrsDMwu.js → access-BHy1v5sr.mjs} +6 -8
  5. package/dist/access-BHy1v5sr.mjs.map +1 -0
  6. package/dist/api-client/index.mjs +3 -0
  7. package/dist/api-client/server.mjs +4 -0
  8. package/dist/{api-client-CgZUMukO.js → api-client-CB7XZaMg.mjs} +8 -10
  9. package/dist/api-client-CB7XZaMg.mjs.map +1 -0
  10. package/dist/auth/auth.server.d.ts +1 -0
  11. package/dist/auth/auth.server.d.ts.map +1 -1
  12. package/dist/auth/index.mjs +1 -0
  13. package/dist/auth/route-protection.server.d.ts.map +1 -1
  14. package/dist/auth/server.mjs +3 -0
  15. package/dist/{identity-C5un-xAs.js → identity/index.mjs} +24 -21
  16. package/dist/identity/index.mjs.map +1 -0
  17. package/dist/{server-Dxsww9v9.js → identity/server.mjs} +7 -8
  18. package/dist/identity/server.mjs.map +1 -0
  19. package/dist/{index.js → index.mjs} +11 -13
  20. package/dist/{logging-DviiAkLx.js → logging/index.mjs} +22 -24
  21. package/dist/logging/index.mjs.map +1 -0
  22. package/dist/{permission-service-CvGpwI0d.js → permission-service-DUyKomU3.mjs} +6 -10
  23. package/dist/permission-service-DUyKomU3.mjs.map +1 -0
  24. package/dist/permissions/index.mjs +3 -0
  25. package/dist/{permissions-_n1hIG7Q.js → permissions-C5dTP9y4.mjs} +23 -15
  26. package/dist/permissions-C5dTP9y4.mjs.map +1 -0
  27. package/dist/query-client/index.mjs +2 -0
  28. package/dist/{query-client-Bh3TyB8W.js → query-client-_xB300gc.mjs} +49 -15
  29. package/dist/query-client-_xB300gc.mjs.map +1 -0
  30. package/dist/react-router/{index.js → index.mjs} +11 -13
  31. package/dist/react-router/server.mjs +17 -0
  32. package/dist/{redis-session-storage.server-v2VcpFsQ.js → redis-session-storage.server-DGgQcJ9B.mjs} +30 -42
  33. package/dist/redis-session-storage.server-DGgQcJ9B.mjs.map +1 -0
  34. package/dist/{server-C96NMFBy.js → server-ByScGRXA.mjs} +39 -36
  35. package/dist/server-ByScGRXA.mjs.map +1 -0
  36. package/dist/{auth.server-D5oiz9aF.js → server-CqX2yCa4.mjs} +388 -119
  37. package/dist/server-CqX2yCa4.mjs.map +1 -0
  38. package/dist/server.mjs +90 -0
  39. package/dist/server.mjs.map +1 -0
  40. package/dist/signalr/index.mjs +2 -0
  41. package/dist/{signalr-k-70VHKo.js → signalr-C-TA8nEE.mjs} +11 -6
  42. package/dist/signalr-C-TA8nEE.mjs.map +1 -0
  43. package/dist/{tenant-DfLemb6n.js → tenant/index.mjs} +19 -12
  44. package/dist/tenant/index.mjs.map +1 -0
  45. package/dist/{server-Ca-3DJf2.js → tenant/server.mjs} +12 -12
  46. package/dist/tenant/server.mjs.map +1 -0
  47. package/dist/{types-BaaE6wjY.js → types-DqCXinAk.mjs} +3 -3
  48. package/dist/types-DqCXinAk.mjs.map +1 -0
  49. package/docs/codex-for-oss-application.md +4 -0
  50. package/package.json +33 -21
  51. package/dist/access/index.js +0 -4
  52. package/dist/access-CGrsDMwu.js.map +0 -1
  53. package/dist/api-client/index.js +0 -5
  54. package/dist/api-client/server.js +0 -6
  55. package/dist/api-client-CgZUMukO.js.map +0 -1
  56. package/dist/auth/index.js +0 -0
  57. package/dist/auth/server.js +0 -6
  58. package/dist/auth.server-D5oiz9aF.js.map +0 -1
  59. package/dist/auth.server-lC94lmlx.js +0 -5
  60. package/dist/identity/index.js +0 -3
  61. package/dist/identity/server.js +0 -5
  62. package/dist/identity-C5un-xAs.js.map +0 -1
  63. package/dist/logging/index.js +0 -3
  64. package/dist/logging-DviiAkLx.js.map +0 -1
  65. package/dist/permission-service-CvGpwI0d.js.map +0 -1
  66. package/dist/permissions/index.js +0 -4
  67. package/dist/permissions-_n1hIG7Q.js.map +0 -1
  68. package/dist/query-client/index.js +0 -3
  69. package/dist/query-client-Bh3TyB8W.js.map +0 -1
  70. package/dist/react-router/server.js +0 -20
  71. package/dist/redis-session-storage.server-v2VcpFsQ.js.map +0 -1
  72. package/dist/server-B6PCxc-T.js +0 -304
  73. package/dist/server-B6PCxc-T.js.map +0 -1
  74. package/dist/server-C96NMFBy.js.map +0 -1
  75. package/dist/server-Ca-3DJf2.js.map +0 -1
  76. package/dist/server-Dxsww9v9.js.map +0 -1
  77. package/dist/server-hGrElJMG.js +0 -78
  78. package/dist/server-hGrElJMG.js.map +0 -1
  79. package/dist/server.js +0 -20
  80. package/dist/signalr/index.js +0 -3
  81. package/dist/signalr-k-70VHKo.js.map +0 -1
  82. package/dist/src-B9rDUgSb.js +0 -0
  83. package/dist/tenant/index.js +0 -3
  84. package/dist/tenant/server.js +0 -5
  85. package/dist/tenant-DfLemb6n.js.map +0 -1
  86. package/dist/types-BaaE6wjY.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/tenant/tenant-store.ts","../../src/tenant/tenant-context.tsx"],"sourcesContent":["/**\n * Tenant Store (Zustand)\n * \n * Client-side state management for tenant data.\n */\n\nimport { create } from 'zustand';\nimport { devtools } from 'zustand/middleware';\nimport type { OrganizationData, TenantData, TenantMembership } from './types';\n\nexport interface TenantDataFetcherContext {\n tenantId: string;\n endpoint: string;\n fetch: typeof fetch;\n}\n\nexport interface TenantSwitchContext {\n tenantId: string;\n endpoint: string;\n fetch: typeof fetch;\n currentTenant: string | null;\n}\n\nexport interface TenantSwitchResult {\n success: boolean;\n tenantData?: TenantData | null;\n reload?: boolean;\n error?: unknown;\n}\n\nexport interface TenantClientConfig {\n tenantDataEndpoint?: string;\n /** @deprecated Use tenantDataEndpoint instead. */\n organizationEndpoint?: string;\n tenantSwitchEndpoint?: string;\n tenantCookieName?: string;\n fetchTenantData?: (context: TenantDataFetcherContext) => Promise<TenantData | null>;\n switchTenant?: (context: TenantSwitchContext) => Promise<TenantSwitchResult>;\n reloadOnSwitch?: boolean;\n}\n\nexport interface TenantState {\n // State\n currentTenant: string | null;\n availableTenants: string[];\n memberships: Map<string, TenantMembership>;\n tenantData: Map<string, TenantData>;\n /** @deprecated Use tenantData instead. */\n organizations: Map<string, OrganizationData>;\n isLoading: boolean;\n isSwitching: boolean;\n\n // Actions\n setCurrentTenant: (tenantId: string | null) => void;\n setAvailableTenants: (tenants: string[]) => void;\n setMemberships: (memberships: TenantMembership[]) => void;\n setTenantData: (tenantId: string, data: TenantData) => void;\n addTenantData: (tenantId: string, data: TenantData) => void;\n /** @deprecated Use setTenantData instead. */\n setOrganization: (tenantId: string, data: OrganizationData) => void;\n /** @deprecated Use addTenantData instead. */\n addOrganization: (tenantId: string, data: OrganizationData) => void;\n setLoading: (loading: boolean) => void;\n setSwitching: (switching: boolean) => void;\n\n // Async actions\n fetchTenantData: (tenantId: string) => Promise<void>;\n /** @deprecated Use fetchTenantData instead. */\n fetchOrganization: (tenantId: string) => Promise<void>;\n switchTenant: (tenantId: string) => Promise<void>;\n\n // Getters\n getMembership: (tenantId: string) => TenantMembership | null;\n getCurrentMembership: () => TenantMembership | null;\n getTenantName: (tenantId: string) => string;\n getTenantData: (tenantId: string) => TenantData | null;\n getCurrentTenantData: () => TenantData | null;\n /** @deprecated Use getTenantData instead. */\n getOrganization: (tenantId: string) => OrganizationData | null;\n /** @deprecated Use getCurrentTenantData instead. */\n getCurrentOrganization: () => OrganizationData | null;\n}\n\n/**\n * Tenant client endpoints and browser-visible tenant cookie (configurable).\n */\nlet tenantDataEndpoint = '/api/tenant/data';\nlet tenantSwitchEndpoint = '/api/tenant/switch';\nlet tenantCookieName = '__spine_tenant';\nlet customFetchTenantData: TenantClientConfig['fetchTenantData'] | null = null;\nlet customSwitchTenant: TenantClientConfig['switchTenant'] | null = null;\nlet reloadOnSwitch = true;\n\n/**\n * Configure tenant client behavior for any backend contract.\n */\nexport function configureTenantClient(config: TenantClientConfig): void {\n if (config.tenantDataEndpoint || config.organizationEndpoint) {\n tenantDataEndpoint = config.tenantDataEndpoint ?? config.organizationEndpoint ?? tenantDataEndpoint;\n }\n if (config.tenantSwitchEndpoint) {\n tenantSwitchEndpoint = config.tenantSwitchEndpoint;\n }\n if (config.tenantCookieName) {\n tenantCookieName = config.tenantCookieName;\n }\n if (config.fetchTenantData) {\n customFetchTenantData = config.fetchTenantData;\n }\n if (config.switchTenant) {\n customSwitchTenant = config.switchTenant;\n }\n if (config.reloadOnSwitch !== undefined) {\n reloadOnSwitch = config.reloadOnSwitch;\n }\n}\n\n/**\n * Configure tenant store endpoints.\n *\n * @deprecated Use configureTenantClient for endpoint and contract customization.\n */\nexport function configureTenantEndpoints(config: TenantClientConfig): void {\n configureTenantClient(config);\n}\n\nexport function getTenantCookieName(): string {\n return tenantCookieName;\n}\n\nexport function resetTenantClientConfig(): void {\n tenantDataEndpoint = '/api/tenant/data';\n tenantSwitchEndpoint = '/api/tenant/switch';\n tenantCookieName = '__spine_tenant';\n customFetchTenantData = null;\n customSwitchTenant = null;\n reloadOnSwitch = true;\n}\n\nfunction asOrganizationMap(tenantDataMap: Map<string, TenantData>): Map<string, OrganizationData> {\n return tenantDataMap as unknown as Map<string, OrganizationData>;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value);\n}\n\nasync function readJsonResponse(response: Response): Promise<unknown> {\n if (response.status === 204) {\n return null;\n }\n\n const text = await response.text();\n return text.trim().length > 0 ? JSON.parse(text) : null;\n}\n\nexport function normalizeTenantDataPayload(payload: unknown): TenantData | null {\n if (!isRecord(payload)) {\n return null;\n }\n\n if (payload.success === false) {\n return null;\n }\n\n const nestedData = payload.data ?? payload.tenant ?? payload.workspace ?? payload.account;\n if (isRecord(nestedData)) {\n return nestedData as TenantData;\n }\n\n return payload as TenantData;\n}\n\nexport function normalizeTenantSwitchPayload(payload: unknown): TenantSwitchResult {\n if (payload === null || payload === undefined) {\n return { success: true };\n }\n\n if (!isRecord(payload)) {\n return { success: false, error: payload };\n }\n\n const success = payload.success ?? payload.ok ?? payload.switched;\n const nestedData = payload.data ?? payload.tenant ?? payload.workspace ?? payload.account;\n\n return {\n success: success === undefined ? true : Boolean(success),\n tenantData: isRecord(nestedData) ? nestedData as TenantData : null,\n reload: typeof payload.reload === 'boolean' ? payload.reload : undefined,\n error: payload.error,\n };\n}\n\nasync function defaultFetchTenantData({ tenantId, endpoint, fetch: fetchFn }: TenantDataFetcherContext): Promise<TenantData | null> {\n const response = await fetchFn(`${endpoint}/${encodeURIComponent(tenantId)}`);\n if (!response.ok) {\n return null;\n }\n\n return normalizeTenantDataPayload(await readJsonResponse(response));\n}\n\nasync function defaultSwitchTenant({ tenantId, endpoint, fetch: fetchFn }: TenantSwitchContext): Promise<TenantSwitchResult> {\n const response = await fetchFn(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ tenantId }),\n });\n\n if (!response.ok) {\n return { success: false, error: response.status };\n }\n\n return normalizeTenantSwitchPayload(await readJsonResponse(response));\n}\n\nexport const useTenantStore = create<TenantState>()(\n devtools(\n (set, get) => ({\n // Initial state\n currentTenant: null,\n availableTenants: [],\n memberships: new Map(),\n tenantData: new Map(),\n organizations: new Map(),\n isLoading: false,\n isSwitching: false,\n\n // Setters\n setCurrentTenant: (tenantId) => set({ currentTenant: tenantId }),\n\n setAvailableTenants: (tenants) => {\n set({ availableTenants: tenants });\n\n // Fetch tenant data for all tenants\n const { tenantData, fetchTenantData } = get();\n tenants.forEach((tenantId) => {\n if (!tenantData.has(tenantId)) {\n fetchTenantData(tenantId);\n }\n });\n },\n\n setMemberships: (memberships) => {\n const membershipMap = new Map<string, TenantMembership>();\n memberships.forEach((membership) => {\n membershipMap.set(membership.tenantId, membership);\n });\n set({ memberships: membershipMap });\n },\n\n setTenantData: (tenantId, data) =>\n set((state) => ({\n tenantData: new Map(state.tenantData).set(tenantId, data),\n organizations: asOrganizationMap(new Map(state.tenantData).set(tenantId, data)),\n })),\n\n addTenantData: (tenantId, data) => {\n set((state) => {\n const newTenantData = new Map(state.tenantData);\n newTenantData.set(tenantId, data);\n return {\n tenantData: newTenantData,\n organizations: asOrganizationMap(newTenantData),\n };\n });\n },\n\n setOrganization: (tenantId, data) => get().setTenantData(tenantId, data),\n addOrganization: (tenantId, data) => get().addTenantData(tenantId, data),\n\n setLoading: (loading) => set({ isLoading: loading }),\n setSwitching: (switching) => set({ isSwitching: switching }),\n\n // Async actions\n fetchTenantData: async (tenantId) => {\n try {\n const tenantData = await (customFetchTenantData ?? defaultFetchTenantData)({\n tenantId,\n endpoint: tenantDataEndpoint,\n fetch,\n });\n\n if (tenantData) {\n get().setTenantData(tenantId, tenantData);\n }\n } catch (error) {\n console.error('Failed to fetch tenant data:', tenantId, error);\n }\n },\n\n fetchOrganization: async (tenantId) => get().fetchTenantData(tenantId),\n\n switchTenant: async (tenantId) => {\n const { availableTenants } = get();\n\n if (!availableTenants.includes(tenantId)) {\n console.error('Tenant not available:', tenantId);\n return;\n }\n\n set({ isSwitching: true });\n\n try {\n const result = await (customSwitchTenant ?? defaultSwitchTenant)({\n tenantId,\n endpoint: tenantSwitchEndpoint,\n fetch,\n currentTenant: get().currentTenant,\n });\n\n if (result.success) {\n set({ currentTenant: tenantId });\n\n if (result.tenantData) {\n get().setTenantData(tenantId, result.tenantData);\n }\n\n // Fetch tenant data if not already loaded\n const { tenantData, fetchTenantData } = get();\n if (!tenantData.has(tenantId)) {\n await fetchTenantData(tenantId);\n }\n\n set({ isSwitching: false });\n\n if ((result.reload ?? reloadOnSwitch) && typeof window !== 'undefined') {\n window.location.reload();\n }\n } else {\n console.error('Failed to switch tenant:', result.error ?? tenantId);\n set({ isSwitching: false });\n }\n } catch (error) {\n console.error('Failed to switch tenant:', error);\n set({ isSwitching: false });\n }\n },\n\n // Getters\n getMembership: (tenantId) => {\n const { memberships } = get();\n return memberships.get(tenantId) || null;\n },\n\n getCurrentMembership: () => {\n const { currentTenant, memberships } = get();\n if (!currentTenant) return null;\n return memberships.get(currentTenant) || null;\n },\n\n getTenantName: (tenantId) => {\n const { memberships, tenantData } = get();\n const membership = memberships.get(tenantId);\n if (membership?.tenantName) {\n return membership.tenantName;\n }\n if (membership?.organizationName) {\n return membership.organizationName;\n }\n return tenantData.get(tenantId)?.name || tenantId;\n },\n\n getTenantData: (tenantId) => {\n const { tenantData } = get();\n return tenantData.get(tenantId) || null;\n },\n\n getCurrentTenantData: () => {\n const { currentTenant, tenantData } = get();\n if (!currentTenant) return null;\n return tenantData.get(currentTenant) || null;\n },\n\n getOrganization: (tenantId) => {\n return get().getTenantData(tenantId) as OrganizationData | null;\n },\n\n getCurrentOrganization: () => {\n return get().getCurrentTenantData() as OrganizationData | null;\n },\n }),\n { name: 'TenantStore' }\n )\n);\n\n/**\n * Initialize store with server data\n */\nexport function initializeTenantStore(\n currentTenant: string | null,\n availableTenants: string[],\n memberships?: TenantMembership[]\n) {\n const store = useTenantStore.getState();\n store.setCurrentTenant(currentTenant);\n store.setAvailableTenants(availableTenants);\n if (memberships) {\n store.setMemberships(memberships);\n }\n}\n","/**\n * Tenant Context Provider\n * \n * React context for tenant management.\n */\n\nimport { createContext, useContext, useEffect, type ReactNode } from 'react';\nimport { getTenantCookieName, useTenantStore } from './tenant-store';\nimport type { OrganizationData, TenantContextType, TenantData, TenantMembership } from './types';\n\nconst TenantContext = createContext<TenantContextType | null>(null);\n\ninterface TenantProviderProps {\n children: ReactNode;\n initialTenant?: string | null;\n initialTenants?: string[];\n initialMemberships?: TenantMembership[];\n onTenantChange?: () => void;\n}\n\nexport function TenantProvider({\n children,\n initialTenant = null,\n initialTenants = [],\n initialMemberships = [],\n onTenantChange,\n}: TenantProviderProps) {\n const currentTenant = useTenantStore((state) => state.currentTenant);\n const availableTenants = useTenantStore((state) => state.availableTenants);\n const memberships = useTenantStore((state) => state.memberships);\n const tenantData = useTenantStore((state) => state.tenantData);\n const organizations = useTenantStore((state) => state.organizations);\n const isLoading = useTenantStore((state) => state.isLoading || state.isSwitching);\n const currentTenantData = useTenantStore((state) => state.getCurrentTenantData());\n const currentMembership = useTenantStore((state) => state.getCurrentMembership());\n\n const setCurrentTenant = useTenantStore((state) => state.setCurrentTenant);\n const setAvailableTenants = useTenantStore((state) => state.setAvailableTenants);\n const setMemberships = useTenantStore((state) => state.setMemberships);\n const switchTenantAction = useTenantStore((state) => state.switchTenant);\n const fetchTenantData = useTenantStore((state) => state.fetchTenantData);\n const getTenantData = useTenantStore((state) => state.getTenantData);\n const getOrganization = useTenantStore((state) => state.getOrganization);\n const getMembership = useTenantStore((state) => state.getMembership);\n const getTenantName = useTenantStore((state) => state.getTenantName);\n\n // Initialize store with initial values\n useEffect(() => {\n if (initialTenant) {\n setCurrentTenant(initialTenant);\n }\n if (initialTenants.length > 0) {\n setAvailableTenants(initialTenants);\n }\n if (initialMemberships.length > 0) {\n setMemberships(initialMemberships);\n }\n }, [\n initialTenant,\n initialTenants.join(','),\n initialMemberships.map((membership) => membership.tenantId).join(','),\n setCurrentTenant,\n setAvailableTenants,\n setMemberships,\n ]);\n\n // Read tenant from cookie (client-side)\n useEffect(() => {\n const getCookieTenant = () => {\n const cookies = document.cookie.split(';');\n const cookieName = getTenantCookieName();\n const tenantCookie = cookies.find((cookie) =>\n cookie.trim().startsWith(`${cookieName}=`)\n );\n\n if (tenantCookie) {\n const value = tenantCookie.split('=')[1];\n return decodeURIComponent(value);\n }\n\n return null;\n };\n\n if (!currentTenant) {\n const cookieTenant = getCookieTenant();\n if (cookieTenant && availableTenants.includes(cookieTenant)) {\n setCurrentTenant(cookieTenant);\n }\n }\n }, [currentTenant, availableTenants, setCurrentTenant]);\n\n // Switch tenant handler\n const switchTenant = (tenantId: string) => {\n switchTenantAction(tenantId).then(() => {\n if (onTenantChange) {\n onTenantChange();\n }\n });\n };\n\n // Refresh tenants (handled by identity context)\n const refreshTenants = () => {\n console.log('Tenant refresh requested - should be handled by identity context');\n };\n\n // Refresh tenant data\n const refreshTenantData = () => {\n if (currentTenant) {\n fetchTenantData(currentTenant);\n }\n };\n\n const refreshOrganization = refreshTenantData;\n\n // Load tenant data when tenant is set\n useEffect(() => {\n if (currentTenant && !currentTenantData) {\n refreshTenantData();\n }\n }, [currentTenant, currentTenantData]);\n\n const currentOrganization = currentTenantData as OrganizationData | null;\n\n const value: TenantContextType = {\n currentTenant,\n availableTenants,\n memberships,\n currentTenantData,\n tenantData,\n currentMembership,\n isLoading,\n switchTenant,\n refreshTenants,\n refreshTenantData,\n getTenantData,\n getMembership,\n getTenantName,\n currentOrganization,\n organizations,\n refreshOrganization,\n getOrganization,\n };\n\n return (\n <TenantContext.Provider value={value}>\n {children}\n </TenantContext.Provider>\n );\n}\n\n/**\n * Hook for using tenant context\n */\nexport function useTenant(): TenantContextType {\n const context = useContext(TenantContext);\n\n if (!context) {\n throw new Error('useTenant must be used within a TenantProvider');\n }\n\n return context;\n}\n\n/**\n * Hook for current tenant only\n */\nexport function useCurrentTenant(): string | null {\n const { currentTenant } = useTenant();\n return currentTenant;\n}\n\n/**\n * Hook for tenant switching\n */\nexport function useTenantSwitcher() {\n const { switchTenant, isLoading, availableTenants } = useTenant();\n return { switchTenant, isLoading, availableTenants };\n}\n\n/**\n * Hook for current tenant data\n */\nexport function useCurrentTenantData(): TenantData | null {\n const { currentTenantData } = useTenant();\n return currentTenantData;\n}\n\n/**\n * Hook for tenant data operations\n */\nexport function useTenantData() {\n const { currentTenantData, refreshTenantData, isLoading } = useTenant();\n\n return {\n tenantData: currentTenantData,\n refreshTenantData,\n isLoading,\n };\n}\n\n/**\n * Hook for current organization\n *\n * @deprecated Use useCurrentTenantData instead.\n */\nexport function useCurrentOrganization(): OrganizationData | null {\n const { currentOrganization } = useTenant();\n return currentOrganization;\n}\n\n/**\n * Hook for organization operations\n *\n * @deprecated Use useTenantData instead.\n */\nexport function useOrganization() {\n const { currentOrganization, refreshOrganization, isLoading } = useTenant();\n\n return {\n organization: currentOrganization,\n refreshOrganization,\n isLoading,\n };\n}\n"],"mappings":";;;;;;;;;;;;;AAsFA,IAAI,qBAAqB;AACzB,IAAI,uBAAuB;AAC3B,IAAI,mBAAmB;AACvB,IAAI,wBAAsE;AAC1E,IAAI,qBAAgE;AACpE,IAAI,iBAAiB;;;;AAKrB,SAAgB,sBAAsB,QAAkC;CACtE,IAAI,OAAO,sBAAsB,OAAO,sBACtC,qBAAqB,OAAO,sBAAsB,OAAO,wBAAwB;CAEnF,IAAI,OAAO,sBACT,uBAAuB,OAAO;CAEhC,IAAI,OAAO,kBACT,mBAAmB,OAAO;CAE5B,IAAI,OAAO,iBACT,wBAAwB,OAAO;CAEjC,IAAI,OAAO,cACT,qBAAqB,OAAO;CAE9B,IAAI,OAAO,mBAAmB,KAAA,GAC5B,iBAAiB,OAAO;AAE5B;;;;;;AAOA,SAAgB,yBAAyB,QAAkC;CACzE,sBAAsB,MAAM;AAC9B;AAEA,SAAgB,sBAA8B;CAC5C,OAAO;AACT;AAEA,SAAgB,0BAAgC;CAC9C,qBAAqB;CACrB,uBAAuB;CACvB,mBAAmB;CACnB,wBAAwB;CACxB,qBAAqB;CACrB,iBAAiB;AACnB;AAEA,SAAS,kBAAkB,eAAuE;CAChG,OAAO;AACT;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,QAAQ,KAAK,KAAK,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,eAAe,iBAAiB,UAAsC;CACpE,IAAI,SAAS,WAAW,KACtB,OAAO;CAGT,MAAM,OAAO,MAAM,SAAS,KAAK;CACjC,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,IAAI,KAAK,MAAM,IAAI,IAAI;AACrD;AAEA,SAAgB,2BAA2B,SAAqC;CAC9E,IAAI,CAAC,SAAS,OAAO,GACnB,OAAO;CAGT,IAAI,QAAQ,YAAY,OACtB,OAAO;CAGT,MAAM,aAAa,QAAQ,QAAQ,QAAQ,UAAU,QAAQ,aAAa,QAAQ;CAClF,IAAI,SAAS,UAAU,GACrB,OAAO;CAGT,OAAO;AACT;AAEA,SAAgB,6BAA6B,SAAsC;CACjF,IAAI,YAAY,QAAQ,YAAY,KAAA,GAClC,OAAO,EAAE,SAAS,KAAK;CAGzB,IAAI,CAAC,SAAS,OAAO,GACnB,OAAO;EAAE,SAAS;EAAO,OAAO;CAAQ;CAG1C,MAAM,UAAU,QAAQ,WAAW,QAAQ,MAAM,QAAQ;CACzD,MAAM,aAAa,QAAQ,QAAQ,QAAQ,UAAU,QAAQ,aAAa,QAAQ;CAElF,OAAO;EACL,SAAS,YAAY,KAAA,IAAY,OAAO,QAAQ,OAAO;EACvD,YAAY,SAAS,UAAU,IAAI,aAA2B;EAC9D,QAAQ,OAAO,QAAQ,WAAW,YAAY,QAAQ,SAAS,KAAA;EAC/D,OAAO,QAAQ;CACjB;AACF;AAEA,eAAe,uBAAuB,EAAE,UAAU,UAAU,OAAO,WAAiE;CAClI,MAAM,WAAW,MAAM,QAAQ,GAAG,SAAS,GAAG,mBAAmB,QAAQ,GAAG;CAC5E,IAAI,CAAC,SAAS,IACZ,OAAO;CAGT,OAAO,2BAA2B,MAAM,iBAAiB,QAAQ,CAAC;AACpE;AAEA,eAAe,oBAAoB,EAAE,UAAU,UAAU,OAAO,WAA6D;CAC3H,MAAM,WAAW,MAAM,QAAQ,UAAU;EACvC,QAAQ;EACR,SAAS,EAAE,gBAAgB,mBAAmB;EAC9C,MAAM,KAAK,UAAU,EAAE,SAAS,CAAC;CACnC,CAAC;CAED,IAAI,CAAC,SAAS,IACZ,OAAO;EAAE,SAAS;EAAO,OAAO,SAAS;CAAO;CAGlD,OAAO,6BAA6B,MAAM,iBAAiB,QAAQ,CAAC;AACtE;AAEA,MAAa,iBAAiB,OAAoB,CAAC,CACjD,UACG,KAAK,SAAS;CAEb,eAAe;CACf,kBAAkB,CAAC;CACnB,6BAAa,IAAI,IAAI;CACrB,4BAAY,IAAI,IAAI;CACpB,+BAAe,IAAI,IAAI;CACvB,WAAW;CACX,aAAa;CAGb,mBAAmB,aAAa,IAAI,EAAE,eAAe,SAAS,CAAC;CAE/D,sBAAsB,YAAY;EAChC,IAAI,EAAE,kBAAkB,QAAQ,CAAC;EAGjC,MAAM,EAAE,YAAY,oBAAoB,IAAI;EAC5C,QAAQ,SAAS,aAAa;GAC5B,IAAI,CAAC,WAAW,IAAI,QAAQ,GAC1B,gBAAgB,QAAQ;EAE5B,CAAC;CACH;CAEA,iBAAiB,gBAAgB;EAC/B,MAAM,gCAAgB,IAAI,IAA8B;EACxD,YAAY,SAAS,eAAe;GAClC,cAAc,IAAI,WAAW,UAAU,UAAU;EACnD,CAAC;EACD,IAAI,EAAE,aAAa,cAAc,CAAC;CACpC;CAEA,gBAAgB,UAAU,SACxB,KAAK,WAAW;EACd,YAAY,IAAI,IAAI,MAAM,UAAU,CAAC,CAAC,IAAI,UAAU,IAAI;EACxD,eAAe,kBAAkB,IAAI,IAAI,MAAM,UAAU,CAAC,CAAC,IAAI,UAAU,IAAI,CAAC;CAChF,EAAE;CAEJ,gBAAgB,UAAU,SAAS;EACjC,KAAK,UAAU;GACb,MAAM,gBAAgB,IAAI,IAAI,MAAM,UAAU;GAC9C,cAAc,IAAI,UAAU,IAAI;GAChC,OAAO;IACL,YAAY;IACZ,eAAe,kBAAkB,aAAa;GAChD;EACF,CAAC;CACH;CAEA,kBAAkB,UAAU,SAAS,IAAI,CAAC,CAAC,cAAc,UAAU,IAAI;CACvE,kBAAkB,UAAU,SAAS,IAAI,CAAC,CAAC,cAAc,UAAU,IAAI;CAEvE,aAAa,YAAY,IAAI,EAAE,WAAW,QAAQ,CAAC;CACnD,eAAe,cAAc,IAAI,EAAE,aAAa,UAAU,CAAC;CAG3D,iBAAiB,OAAO,aAAa;EACnC,IAAI;GACF,MAAM,aAAa,OAAO,yBAAyB,uBAAA,CAAwB;IACzE;IACA,UAAU;IACV;GACF,CAAC;GAED,IAAI,YACF,IAAI,CAAC,CAAC,cAAc,UAAU,UAAU;EAE5C,SAAS,OAAO;GACd,QAAQ,MAAM,gCAAgC,UAAU,KAAK;EAC/D;CACF;CAEA,mBAAmB,OAAO,aAAa,IAAI,CAAC,CAAC,gBAAgB,QAAQ;CAErE,cAAc,OAAO,aAAa;EAChC,MAAM,EAAE,qBAAqB,IAAI;EAEjC,IAAI,CAAC,iBAAiB,SAAS,QAAQ,GAAG;GACxC,QAAQ,MAAM,yBAAyB,QAAQ;GAC/C;EACF;EAEA,IAAI,EAAE,aAAa,KAAK,CAAC;EAEzB,IAAI;GACF,MAAM,SAAS,OAAO,sBAAsB,oBAAA,CAAqB;IAC/D;IACA,UAAU;IACV;IACA,eAAe,IAAI,CAAC,CAAC;GACvB,CAAC;GAED,IAAI,OAAO,SAAS;IAClB,IAAI,EAAE,eAAe,SAAS,CAAC;IAE/B,IAAI,OAAO,YACT,IAAI,CAAC,CAAC,cAAc,UAAU,OAAO,UAAU;IAIjD,MAAM,EAAE,YAAY,oBAAoB,IAAI;IAC5C,IAAI,CAAC,WAAW,IAAI,QAAQ,GAC1B,MAAM,gBAAgB,QAAQ;IAGhC,IAAI,EAAE,aAAa,MAAM,CAAC;IAE1B,KAAK,OAAO,UAAU,mBAAmB,OAAO,WAAW,aACzD,OAAO,SAAS,OAAO;GAE3B,OAAO;IACL,QAAQ,MAAM,4BAA4B,OAAO,SAAS,QAAQ;IAClE,IAAI,EAAE,aAAa,MAAM,CAAC;GAC5B;EACF,SAAS,OAAO;GACd,QAAQ,MAAM,4BAA4B,KAAK;GAC/C,IAAI,EAAE,aAAa,MAAM,CAAC;EAC5B;CACF;CAGA,gBAAgB,aAAa;EAC3B,MAAM,EAAE,gBAAgB,IAAI;EAC5B,OAAO,YAAY,IAAI,QAAQ,KAAK;CACtC;CAEA,4BAA4B;EAC1B,MAAM,EAAE,eAAe,gBAAgB,IAAI;EAC3C,IAAI,CAAC,eAAe,OAAO;EAC3B,OAAO,YAAY,IAAI,aAAa,KAAK;CAC3C;CAEA,gBAAgB,aAAa;EAC3B,MAAM,EAAE,aAAa,eAAe,IAAI;EACxC,MAAM,aAAa,YAAY,IAAI,QAAQ;EAC3C,IAAI,YAAY,YACd,OAAO,WAAW;EAEpB,IAAI,YAAY,kBACd,OAAO,WAAW;EAEpB,OAAO,WAAW,IAAI,QAAQ,CAAC,EAAE,QAAQ;CAC3C;CAEA,gBAAgB,aAAa;EAC3B,MAAM,EAAE,eAAe,IAAI;EAC3B,OAAO,WAAW,IAAI,QAAQ,KAAK;CACrC;CAEA,4BAA4B;EAC1B,MAAM,EAAE,eAAe,eAAe,IAAI;EAC1C,IAAI,CAAC,eAAe,OAAO;EAC3B,OAAO,WAAW,IAAI,aAAa,KAAK;CAC1C;CAEA,kBAAkB,aAAa;EAC7B,OAAO,IAAI,CAAC,CAAC,cAAc,QAAQ;CACrC;CAEA,8BAA8B;EAC5B,OAAO,IAAI,CAAC,CAAC,qBAAqB;CACpC;AACF,IACA,EAAE,MAAM,cAAc,CACxB,CACF;;;;AAKA,SAAgB,sBACd,eACA,kBACA,aACA;CACA,MAAM,QAAQ,eAAe,SAAS;CACtC,MAAM,iBAAiB,aAAa;CACpC,MAAM,oBAAoB,gBAAgB;CAC1C,IAAI,aACF,MAAM,eAAe,WAAW;AAEpC;;;;;;;;ACtYA,MAAM,gBAAgB,cAAwC,IAAI;AAUlE,SAAgB,eAAe,EAC7B,UACA,gBAAgB,MAChB,iBAAiB,CAAC,GAClB,qBAAqB,CAAC,GACtB,kBACsB;CACtB,MAAM,gBAAgB,gBAAgB,UAAU,MAAM,aAAa;CACnE,MAAM,mBAAmB,gBAAgB,UAAU,MAAM,gBAAgB;CACzE,MAAM,cAAc,gBAAgB,UAAU,MAAM,WAAW;CAC/D,MAAM,aAAa,gBAAgB,UAAU,MAAM,UAAU;CAC7D,MAAM,gBAAgB,gBAAgB,UAAU,MAAM,aAAa;CACnE,MAAM,YAAY,gBAAgB,UAAU,MAAM,aAAa,MAAM,WAAW;CAChF,MAAM,oBAAoB,gBAAgB,UAAU,MAAM,qBAAqB,CAAC;CAChF,MAAM,oBAAoB,gBAAgB,UAAU,MAAM,qBAAqB,CAAC;CAEhF,MAAM,mBAAmB,gBAAgB,UAAU,MAAM,gBAAgB;CACzE,MAAM,sBAAsB,gBAAgB,UAAU,MAAM,mBAAmB;CAC/E,MAAM,iBAAiB,gBAAgB,UAAU,MAAM,cAAc;CACrE,MAAM,qBAAqB,gBAAgB,UAAU,MAAM,YAAY;CACvE,MAAM,kBAAkB,gBAAgB,UAAU,MAAM,eAAe;CACvE,MAAM,gBAAgB,gBAAgB,UAAU,MAAM,aAAa;CACnE,MAAM,kBAAkB,gBAAgB,UAAU,MAAM,eAAe;CACvE,MAAM,gBAAgB,gBAAgB,UAAU,MAAM,aAAa;CACnE,MAAM,gBAAgB,gBAAgB,UAAU,MAAM,aAAa;CAGnE,gBAAgB;EACd,IAAI,eACF,iBAAiB,aAAa;EAEhC,IAAI,eAAe,SAAS,GAC1B,oBAAoB,cAAc;EAEpC,IAAI,mBAAmB,SAAS,GAC9B,eAAe,kBAAkB;CAErC,GAAG;EACD;EACA,eAAe,KAAK,GAAG;EACvB,mBAAmB,KAAK,eAAe,WAAW,QAAQ,CAAC,CAAC,KAAK,GAAG;EACpE;EACA;EACA;CACF,CAAC;CAGD,gBAAgB;EACd,MAAM,wBAAwB;GAC5B,MAAM,UAAU,SAAS,OAAO,MAAM,GAAG;GACzC,MAAM,aAAa,oBAAoB;GACvC,MAAM,eAAe,QAAQ,MAAM,WACjC,OAAO,KAAK,CAAC,CAAC,WAAW,GAAG,WAAW,EAAE,CAC3C;GAEA,IAAI,cAAc;IAChB,MAAM,QAAQ,aAAa,MAAM,GAAG,CAAC,CAAC;IACtC,OAAO,mBAAmB,KAAK;GACjC;GAEA,OAAO;EACT;EAEA,IAAI,CAAC,eAAe;GAClB,MAAM,eAAe,gBAAgB;GACrC,IAAI,gBAAgB,iBAAiB,SAAS,YAAY,GACxD,iBAAiB,YAAY;EAEjC;CACF,GAAG;EAAC;EAAe;EAAkB;CAAgB,CAAC;CAGtD,MAAM,gBAAgB,aAAqB;EACzC,mBAAmB,QAAQ,CAAC,CAAC,WAAW;GACtC,IAAI,gBACF,eAAe;EAEnB,CAAC;CACH;CAGA,MAAM,uBAAuB;EAC3B,QAAQ,IAAI,kEAAkE;CAChF;CAGA,MAAM,0BAA0B;EAC9B,IAAI,eACF,gBAAgB,aAAa;CAEjC;CAEA,MAAM,sBAAsB;CAG5B,gBAAgB;EACd,IAAI,iBAAiB,CAAC,mBACpB,kBAAkB;CAEtB,GAAG,CAAC,eAAe,iBAAiB,CAAC;CAIrC,MAAM,QAA2B;EAC/B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,qBAAA;EACA;EACA;EACA;CACF;CAEA,OACE,oBAAC,cAAc,UAAf;EAA+B;EAC5B;CACqB,CAAA;AAE5B;;;;AAKA,SAAgB,YAA+B;CAC7C,MAAM,UAAU,WAAW,aAAa;CAExC,IAAI,CAAC,SACH,MAAM,IAAI,MAAM,gDAAgD;CAGlE,OAAO;AACT;;;;AAKA,SAAgB,mBAAkC;CAChD,MAAM,EAAE,kBAAkB,UAAU;CACpC,OAAO;AACT;;;;AAKA,SAAgB,oBAAoB;CAClC,MAAM,EAAE,cAAc,WAAW,qBAAqB,UAAU;CAChE,OAAO;EAAE;EAAc;EAAW;CAAiB;AACrD;;;;AAKA,SAAgB,uBAA0C;CACxD,MAAM,EAAE,sBAAsB,UAAU;CACxC,OAAO;AACT;;;;AAKA,SAAgB,gBAAgB;CAC9B,MAAM,EAAE,mBAAmB,mBAAmB,cAAc,UAAU;CAEtE,OAAO;EACL,YAAY;EACZ;EACA;CACF;AACF;;;;;;AAOA,SAAgB,yBAAkD;CAChE,MAAM,EAAE,wBAAwB,UAAU;CAC1C,OAAO;AACT;;;;;;AAOA,SAAgB,kBAAkB;CAChC,MAAM,EAAE,qBAAqB,qBAAqB,cAAc,UAAU;CAE1E,OAAO;EACL,cAAc;EACd;EACA;CACF;AACF"}
@@ -1,9 +1,8 @@
1
- import { getAuthSession } from "./redis-session-storage.server-v2VcpFsQ.js";
2
-
1
+ import { p as getAuthSession } from "../redis-session-storage.server-DGgQcJ9B.mjs";
3
2
  //#region src/tenant/tenant-cookie.server.ts
4
3
  const DEFAULT_TENANT_COOKIE_CONFIG = {
5
4
  name: "__spine_tenant",
6
- maxAge: 60 * 60 * 24 * 365,
5
+ maxAge: 3600 * 24 * 365,
7
6
  path: "/",
8
7
  secure: process.env.NODE_ENV === "production",
9
8
  httpOnly: false,
@@ -63,9 +62,13 @@ function configureTenantCookie(config) {
63
62
  function resetTenantCookieConfig() {
64
63
  tenantCookieConfig = { ...DEFAULT_TENANT_COOKIE_CONFIG };
65
64
  }
66
-
67
65
  //#endregion
68
66
  //#region src/tenant/tenant.server.ts
67
+ /**
68
+ * Tenant Server Module
69
+ *
70
+ * Server-side tenant management.
71
+ */
69
72
  let identityContextFetcher = null;
70
73
  let resolveInitialTenant = ({ identityContext }) => {
71
74
  const membership = identityContext?.memberships?.[0];
@@ -156,8 +159,7 @@ async function getAvailableTenants(request) {
156
159
  */
157
160
  async function setCurrentTenant(request, tenantId) {
158
161
  try {
159
- const sessionData = await getAuthSession(request);
160
- if (!sessionData.user) return {
162
+ if (!(await getAuthSession(request)).user) return {
161
163
  headers: new Headers(),
162
164
  success: false,
163
165
  error: "No active session"
@@ -183,10 +185,8 @@ async function setCurrentTenant(request, tenantId) {
183
185
  */
184
186
  async function initializeTenant(request) {
185
187
  try {
186
- const sessionData = await getAuthSession(request);
187
- if (!sessionData?.user) return null;
188
- const currentTenant = await getCurrentTenant(request);
189
- if (currentTenant) return null;
188
+ if (!(await getAuthSession(request))?.user) return null;
189
+ if (await getCurrentTenant(request)) return null;
190
190
  const tenantId = await getTenantFromIdentityContext(request);
191
191
  if (tenantId) {
192
192
  const cookieValue = await setActiveTenant(tenantId);
@@ -212,7 +212,7 @@ async function clearCurrentTenant() {
212
212
  headers.append("Set-Cookie", cookieValue);
213
213
  return headers;
214
214
  }
215
-
216
215
  //#endregion
217
216
  export { clearActiveTenant, clearCurrentTenant, configureIdentityContextFetcher, configureTenantCookie, configureTenantResolution, getActiveTenant, getAvailableTenants, getCurrentTenant, getTenantFromIdentityContext, initializeTenant, resetTenantCookieConfig, resetTenantResolutionConfig, setActiveTenant, setCurrentTenant };
218
- //# sourceMappingURL=server-Ca-3DJf2.js.map
217
+
218
+ //# sourceMappingURL=server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.mjs","names":[],"sources":["../../src/tenant/tenant-cookie.server.ts","../../src/tenant/tenant.server.ts"],"sourcesContent":["/**\n * Tenant Cookie Server Module\n * \n * Cookie-based tenant storage.\n */\n\nexport type TenantCookieSameSite = 'Strict' | 'Lax' | 'None';\n\nexport interface TenantCookieConfig {\n name?: string;\n maxAge?: number;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: TenantCookieSameSite;\n}\n\nconst DEFAULT_TENANT_COOKIE_CONFIG: Required<TenantCookieConfig> = {\n name: '__spine_tenant',\n maxAge: 60 * 60 * 24 * 365,\n path: '/',\n secure: process.env.NODE_ENV === 'production',\n httpOnly: false,\n sameSite: 'Lax',\n};\n\nlet tenantCookieConfig: Required<TenantCookieConfig> = {\n ...DEFAULT_TENANT_COOKIE_CONFIG,\n};\n\nfunction getTenantCookieConfig(): Required<TenantCookieConfig> {\n return tenantCookieConfig;\n}\n\nfunction readCookieValue(cookieHeader: string, name: string): string | null {\n for (const segment of cookieHeader.split(';')) {\n const [rawName, ...rawValue] = segment.split('=');\n if (rawName?.trim() !== name || rawValue.length === 0) {\n continue;\n }\n\n return decodeURIComponent(rawValue.join('='));\n }\n\n return null;\n}\n\n/**\n * Get active tenant from cookie\n */\nexport async function getActiveTenant(request: Request): Promise<string | null> {\n const config = getTenantCookieConfig();\n const cookies = request.headers.get('Cookie');\n if (!cookies) return null;\n\n return readCookieValue(cookies, config.name);\n}\n\n/**\n * Set active tenant cookie value\n */\nexport async function setActiveTenant(tenantId: string): Promise<string> {\n const config = getTenantCookieConfig();\n const secure = config.secure ? '; Secure' : '';\n const httpOnly = config.httpOnly ? '; HttpOnly' : '';\n return `${config.name}=${encodeURIComponent(tenantId)}; Path=${config.path}${httpOnly}; SameSite=${config.sameSite}; Max-Age=${config.maxAge}${secure}`;\n}\n\n/**\n * Clear active tenant cookie value\n */\nexport async function clearActiveTenant(): Promise<string> {\n const config = getTenantCookieConfig();\n const secure = config.secure ? '; Secure' : '';\n const httpOnly = config.httpOnly ? '; HttpOnly' : '';\n return `${config.name}=; Path=${config.path}${httpOnly}; SameSite=${config.sameSite}; Max-Age=0${secure}`;\n}\n\n/**\n * Configure tenant cookie name\n */\nexport function configureTenantCookie(config: TenantCookieConfig): void {\n tenantCookieConfig = {\n ...tenantCookieConfig,\n ...config,\n };\n}\n\n/**\n * Reset tenant cookie config back to defaults.\n */\nexport function resetTenantCookieConfig(): void {\n tenantCookieConfig = {\n ...DEFAULT_TENANT_COOKIE_CONFIG,\n };\n}\n","/**\n * Tenant Server Module\n * \n * Server-side tenant management.\n */\n\nimport { getAuthSession } from '../auth/redis-session-storage.server';\nimport { getActiveTenant, setActiveTenant, clearActiveTenant } from './tenant-cookie.server';\nimport type { SessionData } from '../auth/types';\n\n/**\n * Identity context fetcher type (to avoid circular dependency)\n */\nexport type IdentityContextFetcher = (request: Request) => Promise<{\n memberships?: Array<{ tenantId?: string; id?: string; [key: string]: unknown }>;\n [key: string]: unknown;\n} | null>;\n\nexport interface TenantResolutionContext {\n request: Request;\n session: SessionData;\n identityContext: Awaited<ReturnType<IdentityContextFetcher>>;\n}\n\nexport interface TenantResolutionConfig {\n identityContextFetcher?: IdentityContextFetcher;\n resolveInitialTenant?: (context: TenantResolutionContext) => Promise<string | null> | string | null;\n resolveAvailableTenants?: (context: TenantResolutionContext) => Promise<string[]> | string[];\n}\n\nlet identityContextFetcher: IdentityContextFetcher | null = null;\nlet resolveInitialTenant: NonNullable<TenantResolutionConfig['resolveInitialTenant']> =\n ({ identityContext }) => {\n const membership = identityContext?.memberships?.[0];\n return membership?.tenantId ?? membership?.id ?? null;\n };\nlet resolveAvailableTenants: NonNullable<TenantResolutionConfig['resolveAvailableTenants']> =\n ({ identityContext }) =>\n identityContext?.memberships\n ?.map((membership) => membership.tenantId ?? membership.id)\n .filter((tenantId): tenantId is string => Boolean(tenantId)) ?? [];\n\n/**\n * Configure identity context fetcher\n */\nexport function configureIdentityContextFetcher(fetcher: IdentityContextFetcher): void {\n identityContextFetcher = fetcher;\n}\n\n/**\n * Configure tenant resolution for custom backend identity contracts.\n */\nexport function configureTenantResolution(config: TenantResolutionConfig): void {\n if (config.identityContextFetcher) {\n identityContextFetcher = config.identityContextFetcher;\n }\n if (config.resolveInitialTenant) {\n resolveInitialTenant = config.resolveInitialTenant;\n }\n if (config.resolveAvailableTenants) {\n resolveAvailableTenants = config.resolveAvailableTenants;\n }\n}\n\nexport function resetTenantResolutionConfig(): void {\n identityContextFetcher = null;\n resolveInitialTenant = ({ identityContext }) => {\n const membership = identityContext?.memberships?.[0];\n return membership?.tenantId ?? membership?.id ?? null;\n };\n resolveAvailableTenants = ({ identityContext }) =>\n identityContext?.memberships\n ?.map((membership) => membership.tenantId ?? membership.id)\n .filter((tenantId): tenantId is string => Boolean(tenantId)) ?? [];\n}\n\n/**\n * Get current active tenant from cookie\n */\nexport async function getCurrentTenant(request: Request): Promise<string | null> {\n try {\n const tenantFromCookie = await getActiveTenant(request);\n if (tenantFromCookie) {\n return tenantFromCookie;\n }\n return null;\n } catch (error) {\n console.error('Error getting current tenant:', error);\n return null;\n }\n}\n\n/**\n * Get tenant from identity context\n */\nexport async function getTenantFromIdentityContext(request: Request): Promise<string | null> {\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user?.sub) {\n return null;\n }\n\n if (!identityContextFetcher) {\n console.warn('Identity context fetcher not configured');\n return null;\n }\n\n const context = await identityContextFetcher(request);\n return resolveInitialTenant({ request, session: sessionData, identityContext: context });\n } catch (error) {\n console.error('Error getting tenant from identity context:', error);\n return null;\n }\n}\n\n/**\n * Get available tenants from identity context\n */\nexport async function getAvailableTenants(request: Request): Promise<string[]> {\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user?.sub) {\n return [];\n }\n\n if (!identityContextFetcher) {\n console.warn('Identity context fetcher not configured');\n return [];\n }\n\n const context = await identityContextFetcher(request);\n return resolveAvailableTenants({ request, session: sessionData, identityContext: context });\n } catch (error) {\n console.error('Error getting available tenants:', error);\n return [];\n }\n}\n\n/**\n * Set current active tenant\n */\nexport async function setCurrentTenant(\n request: Request,\n tenantId: string\n): Promise<{ headers: Headers; success: boolean; error?: string }> {\n try {\n const sessionData = await getAuthSession(request);\n\n if (!sessionData.user) {\n return { headers: new Headers(), success: false, error: 'No active session' };\n }\n\n const cookieValue = await setActiveTenant(tenantId);\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n\n return { headers, success: true };\n } catch (error) {\n console.error('Failed to set current tenant:', error);\n return {\n headers: new Headers(),\n success: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n}\n\n/**\n * Initialize tenant on first login\n */\nexport async function initializeTenant(\n request: Request\n): Promise<{ headers: Headers; tenantId: string } | null> {\n try {\n const sessionData = await getAuthSession(request);\n if (!sessionData?.user) {\n return null;\n }\n\n // Check if tenant is already set in cookie\n const currentTenant = await getCurrentTenant(request);\n if (currentTenant) {\n return null;\n }\n\n // Get tenant from identity context\n const tenantId = await getTenantFromIdentityContext(request);\n\n if (tenantId) {\n const cookieValue = await setActiveTenant(tenantId);\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n return { headers, tenantId };\n }\n\n return null;\n } catch (error) {\n console.error('Failed to initialize tenant:', error);\n return null;\n }\n}\n\n/**\n * Clear current tenant\n */\nexport async function clearCurrentTenant(): Promise<Headers> {\n const cookieValue = await clearActiveTenant();\n const headers = new Headers();\n headers.append('Set-Cookie', cookieValue);\n return headers;\n}\n"],"mappings":";;AAiBA,MAAM,+BAA6D;CACjE,MAAM;CACN,QAAQ,OAAU,KAAK;CACvB,MAAM;CACN,QAAQ,QAAQ,IAAI,aAAa;CACjC,UAAU;CACV,UAAU;AACZ;AAEA,IAAI,qBAAmD,EACrD,GAAG,6BACL;AAEA,SAAS,wBAAsD;CAC7D,OAAO;AACT;AAEA,SAAS,gBAAgB,cAAsB,MAA6B;CAC1E,KAAK,MAAM,WAAW,aAAa,MAAM,GAAG,GAAG;EAC7C,MAAM,CAAC,SAAS,GAAG,YAAY,QAAQ,MAAM,GAAG;EAChD,IAAI,SAAS,KAAK,MAAM,QAAQ,SAAS,WAAW,GAClD;EAGF,OAAO,mBAAmB,SAAS,KAAK,GAAG,CAAC;CAC9C;CAEA,OAAO;AACT;;;;AAKA,eAAsB,gBAAgB,SAA0C;CAC9E,MAAM,SAAS,sBAAsB;CACrC,MAAM,UAAU,QAAQ,QAAQ,IAAI,QAAQ;CAC5C,IAAI,CAAC,SAAS,OAAO;CAErB,OAAO,gBAAgB,SAAS,OAAO,IAAI;AAC7C;;;;AAKA,eAAsB,gBAAgB,UAAmC;CACvE,MAAM,SAAS,sBAAsB;CACrC,MAAM,SAAS,OAAO,SAAS,aAAa;CAC5C,MAAM,WAAW,OAAO,WAAW,eAAe;CAClD,OAAO,GAAG,OAAO,KAAK,GAAG,mBAAmB,QAAQ,EAAE,SAAS,OAAO,OAAO,SAAS,aAAa,OAAO,SAAS,YAAY,OAAO,SAAS;AACjJ;;;;AAKA,eAAsB,oBAAqC;CACzD,MAAM,SAAS,sBAAsB;CACrC,MAAM,SAAS,OAAO,SAAS,aAAa;CAC5C,MAAM,WAAW,OAAO,WAAW,eAAe;CAClD,OAAO,GAAG,OAAO,KAAK,UAAU,OAAO,OAAO,SAAS,aAAa,OAAO,SAAS,aAAa;AACnG;;;;AAKA,SAAgB,sBAAsB,QAAkC;CACtE,qBAAqB;EACnB,GAAG;EACH,GAAG;CACL;AACF;;;;AAKA,SAAgB,0BAAgC;CAC9C,qBAAqB,EACnB,GAAG,6BACL;AACF;;;;;;;;ACjEA,IAAI,yBAAwD;AAC5D,IAAI,wBACD,EAAE,sBAAsB;CACvB,MAAM,aAAa,iBAAiB,cAAc;CAClD,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD;AACF,IAAI,2BACD,EAAE,sBACD,iBAAiB,aACb,KAAK,eAAe,WAAW,YAAY,WAAW,EAAE,CAAC,CAC1D,QAAQ,aAAiC,QAAQ,QAAQ,CAAC,KAAK,CAAC;;;;AAKvE,SAAgB,gCAAgC,SAAuC;CACrF,yBAAyB;AAC3B;;;;AAKA,SAAgB,0BAA0B,QAAsC;CAC9E,IAAI,OAAO,wBACT,yBAAyB,OAAO;CAElC,IAAI,OAAO,sBACT,uBAAuB,OAAO;CAEhC,IAAI,OAAO,yBACT,0BAA0B,OAAO;AAErC;AAEA,SAAgB,8BAAoC;CAClD,yBAAyB;CACzB,wBAAwB,EAAE,sBAAsB;EAC9C,MAAM,aAAa,iBAAiB,cAAc;EAClD,OAAO,YAAY,YAAY,YAAY,MAAM;CACnD;CACA,2BAA2B,EAAE,sBAC3B,iBAAiB,aACb,KAAK,eAAe,WAAW,YAAY,WAAW,EAAE,CAAC,CAC1D,QAAQ,aAAiC,QAAQ,QAAQ,CAAC,KAAK,CAAC;AACvE;;;;AAKA,eAAsB,iBAAiB,SAA0C;CAC/E,IAAI;EACF,MAAM,mBAAmB,MAAM,gBAAgB,OAAO;EACtD,IAAI,kBACF,OAAO;EAET,OAAO;CACT,SAAS,OAAO;EACd,QAAQ,MAAM,iCAAiC,KAAK;EACpD,OAAO;CACT;AACF;;;;AAKA,eAAsB,6BAA6B,SAA0C;CAC3F,IAAI;EACF,MAAM,cAAc,MAAM,eAAe,OAAO;EAChD,IAAI,CAAC,aAAa,MAAM,KACtB,OAAO;EAGT,IAAI,CAAC,wBAAwB;GAC3B,QAAQ,KAAK,yCAAyC;GACtD,OAAO;EACT;EAEA,MAAM,UAAU,MAAM,uBAAuB,OAAO;EACpD,OAAO,qBAAqB;GAAE;GAAS,SAAS;GAAa,iBAAiB;EAAQ,CAAC;CACzF,SAAS,OAAO;EACd,QAAQ,MAAM,+CAA+C,KAAK;EAClE,OAAO;CACT;AACF;;;;AAKA,eAAsB,oBAAoB,SAAqC;CAC7E,IAAI;EACF,MAAM,cAAc,MAAM,eAAe,OAAO;EAChD,IAAI,CAAC,aAAa,MAAM,KACtB,OAAO,CAAC;EAGV,IAAI,CAAC,wBAAwB;GAC3B,QAAQ,KAAK,yCAAyC;GACtD,OAAO,CAAC;EACV;EAEA,MAAM,UAAU,MAAM,uBAAuB,OAAO;EACpD,OAAO,wBAAwB;GAAE;GAAS,SAAS;GAAa,iBAAiB;EAAQ,CAAC;CAC5F,SAAS,OAAO;EACd,QAAQ,MAAM,oCAAoC,KAAK;EACvD,OAAO,CAAC;CACV;AACF;;;;AAKA,eAAsB,iBACpB,SACA,UACiE;CACjE,IAAI;EAGF,IAAI,EAAC,MAFqB,eAAe,OAAO,EAAA,CAE/B,MACf,OAAO;GAAE,SAAS,IAAI,QAAQ;GAAG,SAAS;GAAO,OAAO;EAAoB;EAG9E,MAAM,cAAc,MAAM,gBAAgB,QAAQ;EAClD,MAAM,UAAU,IAAI,QAAQ;EAC5B,QAAQ,OAAO,cAAc,WAAW;EAExC,OAAO;GAAE;GAAS,SAAS;EAAK;CAClC,SAAS,OAAO;EACd,QAAQ,MAAM,iCAAiC,KAAK;EACpD,OAAO;GACL,SAAS,IAAI,QAAQ;GACrB,SAAS;GACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU;EAClD;CACF;AACF;;;;AAKA,eAAsB,iBACpB,SACwD;CACxD,IAAI;EAEF,IAAI,EAAC,MADqB,eAAe,OAAO,EAAA,EAC9B,MAChB,OAAO;EAKT,IAAI,MADwB,iBAAiB,OAAO,GAElD,OAAO;EAIT,MAAM,WAAW,MAAM,6BAA6B,OAAO;EAE3D,IAAI,UAAU;GACZ,MAAM,cAAc,MAAM,gBAAgB,QAAQ;GAClD,MAAM,UAAU,IAAI,QAAQ;GAC5B,QAAQ,OAAO,cAAc,WAAW;GACxC,OAAO;IAAE;IAAS;GAAS;EAC7B;EAEA,OAAO;CACT,SAAS,OAAO;EACd,QAAQ,MAAM,gCAAgC,KAAK;EACnD,OAAO;CACT;AACF;;;;AAKA,eAAsB,qBAAuC;CAC3D,MAAM,cAAc,MAAM,kBAAkB;CAC5C,MAAM,UAAU,IAAI,QAAQ;CAC5B,QAAQ,OAAO,cAAc,WAAW;CACxC,OAAO;AACT"}
@@ -2,7 +2,7 @@
2
2
  function isApiClientError(error) {
3
3
  return typeof error === "object" && error !== null && "isApiClientError" in error && error.isApiClientError === true;
4
4
  }
5
-
6
5
  //#endregion
7
- export { isApiClientError };
8
- //# sourceMappingURL=types-BaaE6wjY.js.map
6
+ export { isApiClientError as t };
7
+
8
+ //# sourceMappingURL=types-DqCXinAk.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-DqCXinAk.mjs","names":[],"sources":["../src/api-client/types.ts"],"sourcesContent":["/**\n * API Client Types\n */\n\n// Re-export Logger from logging to avoid duplication\nexport type { LoggerConfig as Logger } from '../logging/types';\n\nexport interface APIConfig {\n basePath: string;\n accessToken?: string;\n headers: Record<string, string>;\n tenantId: string;\n credentials?: RequestCredentials;\n baseOptions?: {\n timeout?: number;\n };\n}\n\nexport interface CreateAPIConfigOptions {\n requireTenant?: boolean;\n includeAuth?: boolean;\n tenantId?: string;\n customHeaders?: Record<string, string>;\n}\n\nexport interface ProblemDetails {\n type: string;\n title: string;\n status: number;\n detail: string;\n traceId: string;\n requestId?: string;\n instance?: string;\n meta?: {\n errors?: ValidationError[];\n [key: string]: unknown;\n };\n}\n\nexport interface ValidationError {\n field: string;\n message: string;\n code?: string;\n}\n\nexport interface RetryConfig {\n maxRetries?: number;\n baseDelay?: number;\n maxDelay?: number;\n retryCondition?: (error: unknown) => boolean;\n}\n\nexport interface APIRequestConfig {\n url?: string;\n method?: string;\n headers?: Record<string, string>;\n}\n\nexport interface APIResponse<T = unknown> {\n data: T;\n status: number;\n statusText: string;\n headers: Record<string, string>;\n raw?: Response;\n}\n\nexport interface APIClientError<T = unknown> extends Error {\n isApiClientError: true;\n response?: APIResponse<T>;\n config?: APIRequestConfig;\n problemDetails?: ProblemDetails;\n shouldLogout?: boolean;\n originalError?: unknown;\n cause?: unknown;\n}\n\nexport function isApiClientError(error: unknown): error is APIClientError {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'isApiClientError' in error &&\n (error as { isApiClientError?: boolean }).isApiClientError === true\n );\n}\n\nexport type FetchAPI = typeof fetch;\n\nexport interface FetchRequestContext {\n fetch: FetchAPI;\n url: string;\n init: RequestInit;\n}\n\nexport interface FetchResponseContext extends FetchRequestContext {\n response: Response;\n}\n\nexport interface FetchErrorContext extends FetchRequestContext {\n error: unknown;\n response?: Response;\n}\n\nexport interface FetchMiddleware {\n pre?: (context: FetchRequestContext) => Promise<{ url: string; init: RequestInit } | void>;\n post?: (context: FetchResponseContext) => Promise<Response | void>;\n onError?: (context: FetchErrorContext) => Promise<Response | void>;\n}\n\nexport type EnhancedAPIClient<T> = {\n [K in keyof T]: T[K] extends (...args: infer A) => Promise<infer R>\n ? K extends `${string}Raw`\n ? T[K]\n : (...args: A) => Promise<APIResponse<R>>\n : T[K];\n};\n\n/**\n * API Logger interface for dependency injection (simplified)\n */\nexport interface APILogger {\n debug?: (message: string, context?: Record<string, unknown>) => void;\n info?: (message: string, context?: Record<string, unknown>) => void;\n warn?: (message: string, error?: Error, context?: Record<string, unknown>) => void;\n error?: (message: string, error?: Error, context?: Record<string, unknown>) => void;\n}\n\n/**\n * Function type for getting access token from request\n */\nexport type GetAccessTokenFn = (request: Request) => Promise<string | null>;\n\n/**\n * Function type for getting current tenant from request\n */\nexport type GetCurrentTenantFn = (request: Request) => Promise<string | null>;\n\n/**\n * Token refresh result\n */\nexport interface TokenRefreshResult {\n success: boolean;\n newAccessToken?: string;\n shouldLogout?: boolean;\n error?: string;\n}\n\n/**\n * Function type for attempting token refresh\n */\nexport type TokenRefreshFn = (request: Request) => Promise<TokenRefreshResult>;\n"],"mappings":";AA4EA,SAAgB,iBAAiB,OAAyC;CACxE,OACE,OAAO,UAAU,YACjB,UAAU,QACV,sBAAsB,SACrB,MAAyC,qBAAqB;AAEnE"}
@@ -26,6 +26,10 @@ Agentic coding is making SaaS apps faster to build, but agents still spend conte
26
26
 
27
27
  Use Codex for core OSS maintenance: PR review, issue triage, release checklist automation, security review of auth/session code, docs drift checks, adapter scaffolding for Next.js/Clerk/Supabase, and regression repair. All changes remain maintainer-reviewed and must pass `pnpm check`.
28
28
 
29
+ ## Program Support Impact Draft
30
+
31
+ Spine is actively developed and still early. Six months of ChatGPT Pro with Codex would directly increase maintainer capacity for OSS work: more adapter coverage, stronger tests, better docs, faster issue triage, and safer auth/session reviews. That support would help turn Spine into a more capable, reliable SaaS package for the open source ecosystem.
32
+
29
33
  ## Anything Else Draft
30
34
 
31
35
  Spine is early-stage, but it solves a common repeated SaaS problem and is especially useful in agentic development workflows. Its goal is to make the reusable infrastructure layer easy for both humans and AI coding agents to understand, install, and extend safely, while keeping product policy in each consuming app.
package/package.json CHANGED
@@ -1,81 +1,81 @@
1
1
  {
2
2
  "name": "@eminuckan/spine",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Framework-agnostic SaaS primitives for auth, identity, multi-tenancy, permissions, and API access.",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.js",
6
+ "main": "./dist/index.mjs",
7
+ "module": "./dist/index.mjs",
8
8
  "types": "./dist/index.d.ts",
9
9
  "packageManager": "pnpm@9.15.9",
10
10
  "sideEffects": false,
11
11
  "exports": {
12
12
  ".": {
13
13
  "types": "./dist/index.d.ts",
14
- "import": "./dist/index.js"
14
+ "import": "./dist/index.mjs"
15
15
  },
16
16
  "./server": {
17
17
  "types": "./dist/server.d.ts",
18
- "import": "./dist/server.js"
18
+ "import": "./dist/server.mjs"
19
19
  },
20
20
  "./react-router": {
21
21
  "types": "./dist/react-router/index.d.ts",
22
- "import": "./dist/react-router/index.js"
22
+ "import": "./dist/react-router/index.mjs"
23
23
  },
24
24
  "./react-router/server": {
25
25
  "types": "./dist/react-router/server.d.ts",
26
- "import": "./dist/react-router/server.js"
26
+ "import": "./dist/react-router/server.mjs"
27
27
  },
28
28
  "./auth": {
29
29
  "types": "./dist/auth/index.d.ts",
30
- "import": "./dist/auth/index.js"
30
+ "import": "./dist/auth/index.mjs"
31
31
  },
32
32
  "./auth/server": {
33
33
  "types": "./dist/auth/server.d.ts",
34
- "import": "./dist/auth/server.js"
34
+ "import": "./dist/auth/server.mjs"
35
35
  },
36
36
  "./access": {
37
37
  "types": "./dist/access/index.d.ts",
38
- "import": "./dist/access/index.js"
38
+ "import": "./dist/access/index.mjs"
39
39
  },
40
40
  "./permissions": {
41
41
  "types": "./dist/permissions/index.d.ts",
42
- "import": "./dist/permissions/index.js"
42
+ "import": "./dist/permissions/index.mjs"
43
43
  },
44
44
  "./tenant": {
45
45
  "types": "./dist/tenant/index.d.ts",
46
- "import": "./dist/tenant/index.js"
46
+ "import": "./dist/tenant/index.mjs"
47
47
  },
48
48
  "./tenant/server": {
49
49
  "types": "./dist/tenant/server.d.ts",
50
- "import": "./dist/tenant/server.js"
50
+ "import": "./dist/tenant/server.mjs"
51
51
  },
52
52
  "./identity": {
53
53
  "types": "./dist/identity/index.d.ts",
54
- "import": "./dist/identity/index.js"
54
+ "import": "./dist/identity/index.mjs"
55
55
  },
56
56
  "./identity/server": {
57
57
  "types": "./dist/identity/server.d.ts",
58
- "import": "./dist/identity/server.js"
58
+ "import": "./dist/identity/server.mjs"
59
59
  },
60
60
  "./api-client": {
61
61
  "types": "./dist/api-client/index.d.ts",
62
- "import": "./dist/api-client/index.js"
62
+ "import": "./dist/api-client/index.mjs"
63
63
  },
64
64
  "./api-client/server": {
65
65
  "types": "./dist/api-client/server.d.ts",
66
- "import": "./dist/api-client/server.js"
66
+ "import": "./dist/api-client/server.mjs"
67
67
  },
68
68
  "./logging": {
69
69
  "types": "./dist/logging/index.d.ts",
70
- "import": "./dist/logging/index.js"
70
+ "import": "./dist/logging/index.mjs"
71
71
  },
72
72
  "./signalr": {
73
73
  "types": "./dist/signalr/index.d.ts",
74
- "import": "./dist/signalr/index.js"
74
+ "import": "./dist/signalr/index.mjs"
75
75
  },
76
76
  "./query-client": {
77
77
  "types": "./dist/query-client/index.d.ts",
78
- "import": "./dist/query-client/index.js"
78
+ "import": "./dist/query-client/index.mjs"
79
79
  }
80
80
  },
81
81
  "files": [
@@ -125,11 +125,23 @@
125
125
  "@types/react": "^18.3.0",
126
126
  "eslint": "^9.0.0",
127
127
  "globals": "^17.4.0",
128
- "tsdown": "^0.9.0",
128
+ "tsdown": "^0.22.4",
129
129
  "typescript": "^5.7.0",
130
130
  "typescript-eslint": "^8.57.0",
131
131
  "vitest": "^4.1.8"
132
132
  },
133
+ "pnpm": {
134
+ "overrides": {
135
+ "@microsoft/signalr>ws": "7.5.11",
136
+ "ajv@6.12.6": "6.14.0",
137
+ "brace-expansion@1.1.12": "1.1.13",
138
+ "brace-expansion@5.0.4": "5.0.6",
139
+ "flatted@3.3.3": "3.4.2",
140
+ "js-yaml@4.1.1": "4.2.0",
141
+ "minimatch@3.1.2": "3.1.4",
142
+ "picomatch@4.0.3": "4.0.4"
143
+ }
144
+ },
133
145
  "engines": {
134
146
  "node": ">=20.0.0"
135
147
  },
@@ -1,4 +0,0 @@
1
- import "../permission-service-CvGpwI0d.js";
2
- import { evaluateAccessRequirement } from "../access-CGrsDMwu.js";
3
-
4
- export { evaluateAccessRequirement };
@@ -1 +0,0 @@
1
- {"version":3,"file":"access-CGrsDMwu.js","names":["EMPTY_DECISION: AccessDecision","value: string","entitlement?: EntitlementAccessSnapshot | null","grantedFeatures: readonly string[]","requiredFeatures: readonly string[]","operator: AccessOperator","requirement: AccessRequirement | null | undefined","subject: AccessSubject","reasons: AccessDeniedReason[]","missingPermissions: string[]","missingFeatures: string[]"],"sources":["../src/access/evaluator.ts"],"sourcesContent":["import { PermissionChecker } from '../permissions/permission-service';\nimport type {\n AccessDecision,\n AccessDeniedReason,\n AccessOperator,\n AccessRequirement,\n AccessSubject,\n EntitlementAccessSnapshot,\n} from './types';\n\nconst ACTIVE_ENTITLEMENT_STATUSES = new Set(['active', 'trialing']);\n\nconst EMPTY_DECISION: AccessDecision = Object.freeze({\n allowed: true,\n reasons: [],\n missingPermissions: [],\n missingFeatures: [],\n});\n\nfunction normalizeFeature(value: string): string {\n return value.trim().toLowerCase();\n}\n\nfunction hasActiveEntitlement(entitlement?: EntitlementAccessSnapshot | null): boolean {\n if (!entitlement?.exists) {\n return false;\n }\n\n return ACTIVE_ENTITLEMENT_STATUSES.has(entitlement.status?.toLowerCase() ?? '');\n}\n\nfunction checkFeatureSet(\n grantedFeatures: readonly string[],\n requiredFeatures: readonly string[],\n operator: AccessOperator,\n): string[] {\n if (requiredFeatures.length === 0) {\n return [];\n }\n\n const granted = new Set(grantedFeatures.map(normalizeFeature));\n const normalizedRequired = requiredFeatures.map(normalizeFeature).filter(Boolean);\n\n if (operator === 'OR') {\n return normalizedRequired.some((feature) => granted.has(feature)) ? [] : normalizedRequired;\n }\n\n return normalizedRequired.filter((feature) => !granted.has(feature));\n}\n\nexport function evaluateAccessRequirement(\n requirement: AccessRequirement | null | undefined,\n subject: AccessSubject,\n): AccessDecision {\n if (!requirement) {\n return EMPTY_DECISION;\n }\n\n const reasons: AccessDeniedReason[] = [];\n const permissions = requirement.permissions ?? [];\n const features = requirement.features ?? [];\n const entitlement = subject.entitlement ?? subject.subscription;\n let missingPermissions: string[] = [];\n let missingFeatures: string[] = [];\n\n if (requirement.internalOnly && !subject.isInternalUser) {\n reasons.push('internal-only');\n }\n\n if (requirement.requireActiveEntitlement && !hasActiveEntitlement(entitlement)) {\n reasons.push('inactive-entitlement');\n } else if (requirement.requireActiveSubscription && !hasActiveEntitlement(entitlement)) {\n reasons.push('inactive-subscription');\n }\n\n if (permissions.length > 0) {\n const checker = new PermissionChecker([...(subject.permissions ?? [])]);\n const result = checker.hasPermissions([...permissions], {\n operator: requirement.permissionOperator ?? 'AND',\n wildcard: true,\n });\n\n if (!result.hasPermission) {\n missingPermissions = result.missingPermissions;\n reasons.push('missing-permissions');\n }\n }\n\n if (features.length > 0) {\n missingFeatures = checkFeatureSet(\n entitlement?.featureKeys ?? [],\n features,\n requirement.featureOperator ?? 'AND',\n );\n\n if (missingFeatures.length > 0) {\n reasons.push('missing-features');\n }\n }\n\n return {\n allowed: reasons.length === 0,\n reasons,\n missingPermissions,\n missingFeatures,\n };\n}\n"],"mappings":";;;AAUA,MAAM,8BAA8B,IAAI,IAAI,CAAC,UAAU,UAAW;AAElE,MAAMA,iBAAiC,OAAO,OAAO;CACnD,SAAS;CACT,SAAS,CAAE;CACX,oBAAoB,CAAE;CACtB,iBAAiB,CAAE;AACpB,EAAC;AAEF,SAAS,iBAAiBC,OAAuB;AAC/C,QAAO,MAAM,MAAM,CAAC,aAAa;AAClC;AAED,SAAS,qBAAqBC,aAAyD;AACrF,MAAK,aAAa,OAChB,QAAO;AAGT,QAAO,4BAA4B,IAAI,YAAY,QAAQ,aAAa,IAAI,GAAG;AAChF;AAED,SAAS,gBACPC,iBACAC,kBACAC,UACU;AACV,KAAI,iBAAiB,WAAW,EAC9B,QAAO,CAAE;CAGX,MAAM,UAAU,IAAI,IAAI,gBAAgB,IAAI,iBAAiB;CAC7D,MAAM,qBAAqB,iBAAiB,IAAI,iBAAiB,CAAC,OAAO,QAAQ;AAEjF,KAAI,aAAa,KACf,QAAO,mBAAmB,KAAK,CAAC,YAAY,QAAQ,IAAI,QAAQ,CAAC,GAAG,CAAE,IAAG;AAG3E,QAAO,mBAAmB,OAAO,CAAC,aAAa,QAAQ,IAAI,QAAQ,CAAC;AACrE;AAED,SAAgB,0BACdC,aACAC,SACgB;AAChB,MAAK,YACH,QAAO;CAGT,MAAMC,UAAgC,CAAE;CACxC,MAAM,cAAc,YAAY,eAAe,CAAE;CACjD,MAAM,WAAW,YAAY,YAAY,CAAE;CAC3C,MAAM,cAAc,QAAQ,eAAe,QAAQ;CACnD,IAAIC,qBAA+B,CAAE;CACrC,IAAIC,kBAA4B,CAAE;AAElC,KAAI,YAAY,iBAAiB,QAAQ,eACvC,SAAQ,KAAK,gBAAgB;AAG/B,KAAI,YAAY,6BAA6B,qBAAqB,YAAY,CAC5E,SAAQ,KAAK,uBAAuB;UAC3B,YAAY,8BAA8B,qBAAqB,YAAY,CACpF,SAAQ,KAAK,wBAAwB;AAGvC,KAAI,YAAY,SAAS,GAAG;EAC1B,MAAM,UAAU,IAAI,kBAAkB,CAAC,GAAI,QAAQ,eAAe,CAAE,CAAE;EACtE,MAAM,SAAS,QAAQ,eAAe,CAAC,GAAG,WAAY,GAAE;GACtD,UAAU,YAAY,sBAAsB;GAC5C,UAAU;EACX,EAAC;AAEF,OAAK,OAAO,eAAe;AACzB,wBAAqB,OAAO;AAC5B,WAAQ,KAAK,sBAAsB;EACpC;CACF;AAED,KAAI,SAAS,SAAS,GAAG;AACvB,oBAAkB,gBAChB,aAAa,eAAe,CAAE,GAC9B,UACA,YAAY,mBAAmB,MAChC;AAED,MAAI,gBAAgB,SAAS,EAC3B,SAAQ,KAAK,mBAAmB;CAEnC;AAED,QAAO;EACL,SAAS,QAAQ,WAAW;EAC5B;EACA;EACA;CACD;AACF"}
@@ -1,5 +0,0 @@
1
- import "../logging-DviiAkLx.js";
2
- import { isApiClientError } from "../types-BaaE6wjY.js";
3
- import { ApiError, ErrorCodes, ErrorHandler, calculateBackoffDelay, createRetryFetch, fetchWithTimeout, handleApiError, isRetryableError, withRetry } from "../api-client-CgZUMukO.js";
4
-
5
- export { ApiError, ErrorCodes, ErrorHandler, calculateBackoffDelay, createRetryFetch, fetchWithTimeout, handleApiError, isApiClientError, isRetryableError, withRetry };
@@ -1,6 +0,0 @@
1
- import "../logging-DviiAkLx.js";
2
- import { isApiClientError } from "../types-BaaE6wjY.js";
3
- import { ApiError, ErrorCodes, ErrorHandler, calculateBackoffDelay, createRetryFetch, fetchWithTimeout, handleApiError, isRetryableError, withRetry } from "../api-client-CgZUMukO.js";
4
- import { createAPIConfigFactory, createApiClient, createFetchMiddleware } from "../server-C96NMFBy.js";
5
-
6
- export { ApiError, ErrorCodes, ErrorHandler, calculateBackoffDelay, createAPIConfigFactory, createApiClient, createFetchMiddleware, createRetryFetch, fetchWithTimeout, handleApiError, isApiClientError, isRetryableError, withRetry };
@@ -1 +0,0 @@
1
- {"version":3,"file":"api-client-CgZUMukO.js","names":["problemDetails: ProblemDetails","response: Response","config: ErrorHandlerConfig","error: ApiError","message: string","type: 'error' | 'success' | 'info'","DEFAULT_RETRY_CONFIG: Required<RetryConfig>","error: unknown","attempt: number","baseDelay: number","maxDelay: number","fn: () => Promise<T>","config?: RetryConfig","lastError: unknown","input: RequestInfo | URL","init?: RequestInit & { timeout?: number }","init?: RequestInit"],"sources":["../src/api-client/error-codes.ts","../src/api-client/error-handler.ts","../src/api-client/retry-handler.ts"],"sourcesContent":["/**\n * Error Codes\n * \n * Generic error codes that apps can use as a starter vocabulary.\n * Domain-specific codes should live in the consuming app.\n */\n\nexport const ErrorCodes = {\n // Validation\n Validation: {\n Failed: 'ValidationFailed',\n },\n\n // Identity\n Identity: {\n InvalidCredentials: 'InvalidCredentials',\n UserInactive: 'UserInactive',\n TenantContextMissing: 'TenantContextMissing',\n InvalidCurrentPassword: 'InvalidCurrentPassword',\n OwnerCannotDisableMembership: 'OwnerCannotDisableMembership',\n CurrentUserMissing: 'CurrentUserMissing',\n UserNotFound: 'UserNotFound',\n EmailAlreadyExists: 'EmailAlreadyExists',\n },\n\n // Access and entitlement\n Access: {\n EntitlementRequired: 'EntitlementRequired',\n EntitlementExpired: 'EntitlementExpired',\n FeatureNotAvailable: 'FeatureNotAvailable',\n TenantContextMissing: 'TenantContextMissing',\n },\n\n // General\n NotFound: 'NotFound',\n Unauthorized: 'Unauthorized',\n Forbidden: 'Forbidden',\n InternalError: 'InternalError',\n} as const;\n","/**\n * Error Handler\n * \n * Centralized error handling with ProblemDetails parsing.\n */\n\nimport type { ProblemDetails, ValidationError } from './types';\nimport { ErrorCodes } from './error-codes';\nimport { logger } from '../logging';\n\n/**\n * API Error Class\n */\nexport class ApiError extends Error {\n constructor(\n public problemDetails: ProblemDetails,\n public response: Response\n ) {\n super(problemDetails.detail);\n this.name = 'ApiError';\n }\n\n get code(): string {\n return this.problemDetails.title;\n }\n\n get status(): number {\n return this.problemDetails.status;\n }\n\n get isValidationError(): boolean {\n return this.code === ErrorCodes.Validation.Failed;\n }\n\n get validationErrors(): ValidationError[] {\n return this.problemDetails.meta?.errors ?? [];\n }\n\n get traceId(): string {\n return this.problemDetails.traceId;\n }\n}\n\n/**\n * Parse API error response\n */\nexport async function handleApiError(response: Response): Promise<never> {\n let problemDetails: ProblemDetails;\n\n try {\n problemDetails = await response.json();\n } catch {\n problemDetails = {\n type: `https://httpstatuses.com/${response.status}`,\n title: 'UnknownError',\n status: response.status,\n detail: response.statusText || 'An unknown error occurred',\n traceId: response.headers.get('x-trace-id') ?? 'unknown',\n };\n }\n\n logger.error('API request failed', undefined, {\n code: problemDetails.title,\n status: problemDetails.status,\n detail: problemDetails.detail,\n traceId: problemDetails.traceId,\n });\n\n throw new ApiError(problemDetails, response);\n}\n\n/**\n * Error Handler Configuration\n */\nexport interface ErrorHandlerConfig {\n handlers?: Partial<Record<string, (error: ApiError) => void>>;\n showToast?: boolean;\n toastMessages?: Partial<Record<string, string>>;\n onError?: (error: ApiError) => void;\n toast?: (message: string, type: 'error' | 'success' | 'info') => void;\n}\n\n/**\n * Error Handler Class\n */\nexport class ErrorHandler {\n private config: ErrorHandlerConfig;\n\n constructor(config: ErrorHandlerConfig = {}) {\n this.config = {\n showToast: true,\n ...config,\n };\n }\n\n handle(error: ApiError): void {\n const code = error.code;\n\n logger.error('Handling API Error', undefined, {\n code,\n status: error.status,\n traceId: error.traceId,\n });\n\n // Try custom handler first\n if (this.config.handlers?.[code]) {\n this.config.handlers[code]!(error);\n return;\n }\n\n // Fallback\n if (this.config.onError) {\n this.config.onError(error);\n } else {\n this.showDefaultToast(error);\n }\n }\n\n private showToast(message: string, type: 'error' | 'success' | 'info'): void {\n if (this.config.showToast && this.config.toast) {\n this.config.toast(message, type);\n }\n }\n\n private showDefaultToast(error: ApiError): void {\n const message =\n this.config.toastMessages?.[error.code] ||\n error.problemDetails.detail ||\n 'An error occurred';\n this.showToast(message, 'error');\n }\n}\n\n/**\n * Create global error handler\n */\nexport function createErrorHandler(config?: ErrorHandlerConfig): ErrorHandler {\n return new ErrorHandler(config);\n}\n\n/**\n * Map validation errors to form fields\n */\nexport function mapValidationErrors(errors: ValidationError[]): Record<string, string> {\n const mapped: Record<string, string> = {};\n for (const error of errors) {\n const fieldName = error.field.charAt(0).toLowerCase() + error.field.slice(1);\n mapped[fieldName] = error.message;\n }\n return mapped;\n}\n","/**\n * Retry Handler\n * \n * Retry logic for API requests.\n */\n\nimport type { RetryConfig } from './types';\n\nconst DEFAULT_RETRY_CONFIG: Required<RetryConfig> = {\n maxRetries: 3,\n baseDelay: 1000,\n maxDelay: 10000,\n retryCondition: (error: unknown) => {\n if (error instanceof Response) {\n return error.status >= 500 || error.status === 429;\n }\n return false;\n },\n};\n\n/**\n * Calculate backoff delay\n */\nexport function calculateBackoffDelay(\n attempt: number,\n baseDelay: number,\n maxDelay: number\n): number {\n const delay = baseDelay * Math.pow(2, attempt);\n const jitter = Math.random() * 0.3 * delay;\n return Math.min(delay + jitter, maxDelay);\n}\n\n/**\n * Check if error is retryable\n */\nexport function isRetryableError(error: unknown): boolean {\n if (error instanceof Response) {\n return error.status >= 500 || error.status === 429;\n }\n if (error instanceof TypeError) {\n // Network errors\n return true;\n }\n return false;\n}\n\n/**\n * Execute function with retry logic\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n config?: RetryConfig\n): Promise<T> {\n const { maxRetries, baseDelay, maxDelay, retryCondition } = {\n ...DEFAULT_RETRY_CONFIG,\n ...config,\n };\n\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n\n if (attempt === maxRetries || !retryCondition(error)) {\n throw error;\n }\n\n const delay = calculateBackoffDelay(attempt, baseDelay, maxDelay);\n console.log(`Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n\n throw lastError;\n}\n\n/**\n * Fetch with timeout\n */\nexport async function fetchWithTimeout(\n input: RequestInfo | URL,\n init?: RequestInit & { timeout?: number }\n): Promise<Response> {\n const { timeout = 30000, ...fetchInit } = init || {};\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n try {\n const response = await fetch(input, {\n ...fetchInit,\n signal: controller.signal,\n });\n return response;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\n/**\n * Create fetch function with retry\n */\nexport function createRetryFetch(config?: RetryConfig) {\n return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n return withRetry(() => fetch(input, init), config);\n };\n}\n"],"mappings":"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"}
File without changes
@@ -1,6 +0,0 @@
1
- import "../logging-DviiAkLx.js";
2
- import { AUTH_ERROR_COOKIE_PREFIX, cleanupExpiredOAuthStates, closeRedisConnection, createAuthSession, createOAuthState, deleteOAuthState, destroyAuthSession, destroyAuthSessionById, destroyAuthSessionsByIdentitySession, destroyAuthSessionsBySessionState, destroyAuthSessionsBySid, destroyAuthSessionsForUser, getAuthSession, getAuthSessionById, getCurrentAuthSessionId, getOAuthState, isSessionValid, listAuthSessionDataForUser, listAuthSessionsForUser, listCurrentUserAuthSessions, requireAuthSession, updateAuthSession } from "../redis-session-storage.server-v2VcpFsQ.js";
3
- import { clearAuthErrorHeaders, clearAuthServerCache, configureAuthClaimMapping, extractUserInfo, getAccessToken, getAuthError, getUser, handleBackChannelLogout, handleCallback, handleFrontChannelLogout, hasAuthError, login, logout, refreshTokens, requireAuth, resetAuthClaimMapping } from "../auth.server-D5oiz9aF.js";
4
- import { attemptTokenRefresh, authRoute, configureRouteProtection, isTokenExpiredResponse, onboardingRoute, policyRoute, protectRoute, protectedRoute, publicRoute, resetRouteProtectionConfig, shouldAttemptTokenRefresh, shouldRefreshToken, subscriptionRoute } from "../server-B6PCxc-T.js";
5
-
6
- export { AUTH_ERROR_COOKIE_PREFIX, attemptTokenRefresh, authRoute, cleanupExpiredOAuthStates, clearAuthErrorHeaders, clearAuthServerCache, closeRedisConnection, configureAuthClaimMapping, configureRouteProtection, createAuthSession, createOAuthState, deleteOAuthState, destroyAuthSession, destroyAuthSessionById, destroyAuthSessionsByIdentitySession, destroyAuthSessionsBySessionState, destroyAuthSessionsBySid, destroyAuthSessionsForUser, extractUserInfo, getAccessToken, getAuthError, getAuthSession, getAuthSessionById, getCurrentAuthSessionId, getOAuthState, getUser, handleBackChannelLogout, handleCallback, handleFrontChannelLogout, hasAuthError, isSessionValid, isTokenExpiredResponse, listAuthSessionDataForUser, listAuthSessionsForUser, listCurrentUserAuthSessions, login, logout, onboardingRoute, policyRoute, protectRoute, protectedRoute, publicRoute, refreshTokens, requireAuth, requireAuthSession, resetAuthClaimMapping, resetRouteProtectionConfig, shouldAttemptTokenRefresh, shouldRefreshToken, subscriptionRoute, updateAuthSession };
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth.server-D5oiz9aF.js","names":["location: string","init: RedirectResponseInit","key: string","allowedNames: Set<string>","allowedPrefixes: string[]","params: LoginOptions['extraAuthParams'] | undefined","options: Pick<LoginOptions, 'extraAuthParamNames' | 'extraAuthParamPrefixes'>","sanitized: Record<string, string>","context: LoginOptions['publicStateContext']","normalized: Record<string, string>","baseState: string","publicStateContext: LoginOptions['publicStateContext']","config: AuthConfig","applicationType: ApplicationType","value: string | undefined","fallback: OidcClientAuthMethod","OIDC_CONFIG: AuthConfig | null","authServerCache: {\n configuration: oidc.Configuration;\n cachedAt: number;\n} | null","token: string | undefined","idToken: string | undefined","expectedIssuer: string","returnUrl: string | null","request: Request","returnUrl: string","action: string | null | undefined","status: string | null | undefined","path: string","authority: string","error: unknown","url: URL","redirectUrl: string","configuration: oidc.Configuration","tokenTypeHint: 'access_token' | 'refresh_token'","sessionData: SessionData","userId: string","tokenResult: OidcTokenResponse","jwksUri: string","payload: Record<string, unknown>","clientId: string","DEFAULT_AUTH_CLAIM_MAPPING: Required<AuthClaimMapping>","authClaimMapping: Required<AuthClaimMapping>","keys?: string[]","mapping: AuthClaimMapping","claims: Record<string, unknown>","claimKeys: string[]","value: string","value: unknown","tenants: string[]","tenantRoles: Record<string, string[]>","parsedRoles: unknown","baseUser: UserInfo","returnUrlOrOptions?: string | LoginOptions","options: LoginOptions","headers","oauthState: OAuthState","authorizationParameters: Record<string, string>","cookies","oauthStateRegex","cookieMatch","stateId","oauthState","stateAge","redirectUrl","redirectUri","baseUrl","logoutParameters: Record<string, string>","result: BackChannelLogoutResult","result: FrontChannelLogoutResult","refreshToken?: string"],"sources":["../src/http/response.ts","../src/auth/authorization-parameters.ts","../src/auth/auth.server.ts"],"sourcesContent":["/**\n * HTTP response helpers that stay framework-agnostic.\n */\n\nexport interface RedirectResponseInit {\n headers?: HeadersInit;\n status?: number;\n}\n\n/**\n * Create a redirect response without relying on a router/framework helper.\n */\nexport function createRedirectResponse(\n location: string,\n init: RedirectResponseInit = {}\n): Response {\n const headers = new Headers(init.headers);\n headers.set('Location', location);\n\n return new Response(null, {\n status: init.status ?? 302,\n headers,\n });\n}\n","import type { LoginOptions } from './types';\n\nconst RESERVED_AUTHORIZATION_PARAMETER_NAMES = new Set([\n 'client_id',\n 'code_challenge',\n 'code_challenge_method',\n 'kc_action',\n 'nonce',\n 'prompt',\n 'redirect_uri',\n 'response_mode',\n 'response_type',\n 'scope',\n 'state',\n]);\n\nconst DEFAULT_EXTRA_AUTHORIZATION_PARAMETER_NAMES = new Set([\n 'acr_values',\n 'kc_idp_hint',\n 'login_hint',\n 'ui_locales',\n]);\n\nfunction isAllowedExtraAuthorizationParameter(\n key: string,\n allowedNames: Set<string>,\n allowedPrefixes: string[]\n): boolean {\n const normalized = key.toLowerCase();\n return allowedNames.has(normalized) || allowedPrefixes.some((prefix) => normalized.startsWith(prefix));\n}\n\nexport function sanitizeExtraAuthorizationParameters(\n params: LoginOptions['extraAuthParams'] | undefined,\n options: Pick<LoginOptions, 'extraAuthParamNames' | 'extraAuthParamPrefixes'> = {}\n): Record<string, string> {\n if (!params) {\n return {};\n }\n\n const allowedNames = new Set([\n ...DEFAULT_EXTRA_AUTHORIZATION_PARAMETER_NAMES,\n ...(options.extraAuthParamNames ?? []).map((name) => name.trim().toLowerCase()).filter(Boolean),\n ]);\n const allowedPrefixes = (options.extraAuthParamPrefixes ?? [])\n .map((prefix) => prefix.trim().toLowerCase())\n .filter(Boolean);\n const sanitized: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(params)) {\n const normalizedKey = key.trim();\n if (\n normalizedKey.length === 0 ||\n RESERVED_AUTHORIZATION_PARAMETER_NAMES.has(normalizedKey.toLowerCase()) ||\n !isAllowedExtraAuthorizationParameter(normalizedKey, allowedNames, allowedPrefixes) ||\n value === null ||\n value === undefined\n ) {\n continue;\n }\n\n const normalizedValue = String(value).trim();\n if (normalizedValue.length === 0) {\n continue;\n }\n\n sanitized[normalizedKey] = normalizedValue;\n }\n\n return sanitized;\n}\n\nexport function normalizePublicAuthorizationStateContext(\n context: LoginOptions['publicStateContext']\n): Record<string, string> | null {\n if (!context) {\n return null;\n }\n\n const normalized: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(context)) {\n const normalizedKey = key.trim();\n if (\n normalizedKey.length === 0 ||\n RESERVED_AUTHORIZATION_PARAMETER_NAMES.has(normalizedKey.toLowerCase()) ||\n value === null ||\n value === undefined\n ) {\n continue;\n }\n\n const normalizedValue = String(value).trim();\n if (normalizedValue.length === 0) {\n continue;\n }\n\n normalized[normalizedKey] = normalizedValue;\n }\n\n return Object.keys(normalized).length > 0 ? normalized : null;\n}\n\nexport function buildAuthorizationState(\n baseState: string,\n publicStateContext: LoginOptions['publicStateContext']\n): string {\n const normalizedContext = normalizePublicAuthorizationStateContext(publicStateContext);\n if (!normalizedContext) {\n return baseState;\n }\n\n return `${baseState}.${Buffer.from(JSON.stringify(normalizedContext), 'utf8').toString('base64url')}`;\n}\n","/**\n * OpenID Connect Authentication Server Module\n *\n * Handles:\n * - Login flow (PKCE)\n * - Token exchange\n * - Token refresh\n * - Logout\n * - Session management\n */\n\nimport * as oidc from 'openid-client';\nimport { createRemoteJWKSet, jwtVerify } from 'jose';\n\nimport {\n createAuthSession,\n getAuthSession,\n updateAuthSession,\n destroyAuthSession,\n isSessionValid,\n listAuthSessionDataForUser,\n destroyAuthSessionsByIdentitySession,\n destroyAuthSessionsBySid,\n destroyAuthSessionsForUser,\n createOAuthState,\n getOAuthState,\n deleteOAuthState,\n AUTH_ERROR_COOKIE_PREFIX,\n} from './redis-session-storage.server';\nimport type { \n AuthConfig, \n AuthClaimMapping,\n UserInfo, \n SessionData, \n OAuthState,\n TokenRefreshResult,\n LoginOptions,\n ApplicationType,\n OidcClientAuthMethod,\n BackChannelLogoutResult,\n FrontChannelLogoutResult,\n AuthError\n} from './types';\nimport { logger } from '../logging';\nimport { createRedirectResponse } from '../http/response';\nimport {\n buildAuthorizationState,\n sanitizeExtraAuthorizationParameters,\n} from './authorization-parameters';\n\ntype OidcTokenResponse = oidc.TokenEndpointResponse & oidc.TokenEndpointResponseHelpers;\ntype LogoutScope = 'identity' | 'local' | 'all';\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\n/**\n * Get effective hasLandingPage setting based on config\n */\nfunction getEffectiveHasLandingPage(config: AuthConfig): boolean {\n // Explicit setting takes precedence\n if (config.hasLandingPage !== undefined) {\n return config.hasLandingPage;\n }\n \n // Determine by application type\n switch (config.applicationType) {\n case 'no-landing-page':\n case 'dashboard':\n return false; // Dashboard has no landing page\n case 'landing-page':\n case 'tenant-app':\n return true; // Tenant app has landing page\n default:\n return true; // Default to having a landing page\n }\n}\n\n/**\n * Create OAuth configuration from environment variables\n */\nfunction createAuthConfig(): AuthConfig {\n const requiredEnvVars = {\n authority: process.env.OIDC_AUTHORITY,\n clientId: process.env.OIDC_CLIENT_ID,\n redirectUri: process.env.OIDC_REDIRECT_URI,\n };\n\n for (const [key, value] of Object.entries(requiredEnvVars)) {\n if (!value) {\n throw new Error(`Missing required environment variable: OIDC_${key.toUpperCase()}`);\n }\n }\n\n // Parse application type from env\n const appTypeEnv = process.env.OIDC_APPLICATION_TYPE?.toLowerCase();\n let applicationType: ApplicationType = 'custom';\n if (appTypeEnv === 'no-landing-page' || appTypeEnv === 'no_landing_page') {\n applicationType = 'no-landing-page';\n } else if (appTypeEnv === 'landing-page' || appTypeEnv === 'landing_page') {\n applicationType = 'landing-page';\n } else if (appTypeEnv === 'dashboard') {\n applicationType = 'dashboard';\n } else if (appTypeEnv === 'tenant-app' || appTypeEnv === 'tenant_app') {\n applicationType = 'tenant-app';\n }\n\n const clientAuthMethod = parseOidcClientAuthMethod(\n process.env.OIDC_CLIENT_AUTH_METHOD,\n process.env.OIDC_CLIENT_SECRET ? 'client_secret_post' : 'none'\n );\n\n const hasLandingPage = process.env.OIDC_HAS_LANDING_PAGE !== undefined\n ? process.env.OIDC_HAS_LANDING_PAGE === 'true'\n : undefined;\n\n return {\n authority: requiredEnvVars.authority!,\n clientId: requiredEnvVars.clientId!,\n clientSecret: process.env.OIDC_CLIENT_SECRET,\n clientAuthMethod,\n redirectUri: requiredEnvVars.redirectUri!,\n scope: process.env.OIDC_SCOPE || 'openid profile email api',\n postLogoutRedirectUri: process.env.OIDC_POST_LOGOUT_REDIRECT_URI,\n applicationType,\n hasLandingPage,\n };\n}\n\nfunction parseOidcClientAuthMethod(\n value: string | undefined,\n fallback: OidcClientAuthMethod\n): OidcClientAuthMethod {\n const normalized = value?.trim().toLowerCase();\n\n switch (normalized) {\n case undefined:\n case '':\n return fallback;\n case 'none':\n return 'none';\n case 'client_secret_post':\n case 'post':\n return 'client_secret_post';\n case 'client_secret_basic':\n case 'basic':\n return 'client_secret_basic';\n default:\n throw new Error(`Unsupported OIDC_CLIENT_AUTH_METHOD: ${value}`);\n }\n}\n\nlet OIDC_CONFIG: AuthConfig | null = null;\n\nfunction getAuthConfig(): AuthConfig {\n if (!OIDC_CONFIG) {\n OIDC_CONFIG = createAuthConfig();\n }\n return OIDC_CONFIG;\n}\n\n// ============================================================================\n// OAuth Discovery Cache\n// ============================================================================\n\nlet authServerCache: {\n configuration: oidc.Configuration;\n cachedAt: number;\n} | null = null;\n\nconst CACHE_TTL = 60 * 60 * 1000; // 1 hour\nconst jwksCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\nfunction normalizeIssuer(value: string | undefined): string | null {\n const normalized = value?.trim().replace(/\\/+$/, '');\n return normalized && normalized.length > 0 ? normalized : null;\n}\n\nfunction getJwtClaims(token: string | undefined): Record<string, unknown> | null {\n if (!token) {\n return null;\n }\n\n try {\n const [, payload] = token.split('.');\n if (!payload) {\n return null;\n }\n\n return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nfunction getJwtIssuer(token: string | undefined): string | null {\n const issuer = getJwtClaims(token)?.iss;\n return typeof issuer === 'string' ? normalizeIssuer(issuer) : null;\n}\n\nfunction canUseIdTokenHint(idToken: string | undefined, expectedIssuer: string): boolean {\n if (!idToken) {\n return false;\n }\n\n const tokenIssuer = getJwtIssuer(idToken);\n return tokenIssuer !== null && tokenIssuer === normalizeIssuer(expectedIssuer);\n}\n\nfunction sanitizeLogoutReturnUrl(returnUrl: string | null, request: Request): string | null {\n if (!returnUrl) {\n return null;\n }\n\n try {\n const requestUrl = new URL(request.url);\n const decodedReturnUrl = decodeURIComponent(returnUrl);\n\n if (decodedReturnUrl.startsWith('/') && !decodedReturnUrl.startsWith('//')) {\n return isAuthLoopPath(decodedReturnUrl) ? null : decodedReturnUrl;\n }\n\n const candidateUrl = new URL(decodedReturnUrl);\n if (candidateUrl.origin !== requestUrl.origin) {\n return null;\n }\n\n const sameOriginPath = `${candidateUrl.pathname}${candidateUrl.search}${candidateUrl.hash}`;\n return isAuthLoopPath(sameOriginPath) ? null : sameOriginPath;\n } catch {\n return null;\n }\n}\n\nfunction appendApplicationInitiatedActionResult(\n returnUrl: string,\n action: string | null | undefined,\n status: string | null | undefined\n): string {\n if (!action && !status) {\n return returnUrl;\n }\n\n try {\n const isRelative = returnUrl.startsWith('/') && !returnUrl.startsWith('//');\n const url = new URL(returnUrl, isRelative ? 'http://spine.local' : undefined);\n\n if (action) {\n url.searchParams.set('kc_action', action);\n }\n\n if (status) {\n url.searchParams.set('kc_action_status', status);\n }\n\n return isRelative ? `${url.pathname}${url.search}${url.hash}` : url.toString();\n } catch {\n const params = new URLSearchParams();\n if (action) params.set('kc_action', action);\n if (status) params.set('kc_action_status', status);\n const separator = returnUrl.includes('?') ? '&' : '?';\n return `${returnUrl}${separator}${params.toString()}`;\n }\n}\n\nfunction isAuthLoopPath(path: string): boolean {\n const normalized = path.split('#')[0].split('?')[0].replace(/\\/+$/, '') || '/';\n return normalized === '/auth/logout' || normalized === '/auth/callback';\n}\n\nfunction isLocalInsecureIssuer(authority: string): boolean {\n try {\n const issuer = new URL(authority);\n return (\n issuer.protocol === 'http:' &&\n ['localhost', '127.0.0.1', '::1', '[::1]'].includes(issuer.hostname)\n );\n } catch {\n return false;\n }\n}\n\nfunction shouldAllowInsecureOidcRequests(authority: string): boolean {\n if (process.env.NODE_ENV === 'production') {\n return false;\n }\n\n if (process.env.OIDC_ALLOW_INSECURE_REQUESTS === 'true') {\n return true;\n }\n\n return isLocalInsecureIssuer(authority);\n}\n\nfunction getDiscoveryOptions(): oidc.DiscoveryRequestOptions {\n const config = getAuthConfig();\n\n return shouldAllowInsecureOidcRequests(config.authority)\n ? { algorithm: 'oidc', execute: [oidc.allowInsecureRequests] }\n : { algorithm: 'oidc' };\n}\n\nfunction getClientMetadata(config: AuthConfig): string | undefined {\n return config.clientAuthMethod === 'none' ? undefined : config.clientSecret;\n}\n\nfunction getClientAuthentication(config: AuthConfig): oidc.ClientAuth {\n switch (config.clientAuthMethod) {\n case 'client_secret_basic':\n if (!config.clientSecret) {\n throw new Error('OIDC_CLIENT_SECRET is required for client_secret_basic authentication');\n }\n\n return oidc.ClientSecretBasic(config.clientSecret);\n case 'client_secret_post':\n if (!config.clientSecret) {\n throw new Error('OIDC_CLIENT_SECRET is required for client_secret_post authentication');\n }\n\n return oidc.ClientSecretPost(config.clientSecret);\n case 'none':\n default:\n return oidc.None();\n }\n}\n\nfunction getOAuthErrorMessage(error: unknown): string {\n if (error instanceof oidc.ResponseBodyError) {\n return error.error_description || error.error || error.message;\n }\n\n if (error instanceof Error) {\n return error.message;\n }\n\n return 'Unknown error';\n}\n\nfunction shouldLogoutAfterOAuthError(error: unknown): boolean {\n if (error instanceof oidc.ResponseBodyError) {\n return (\n error.status === 400 ||\n error.status === 401 ||\n error.error === 'invalid_grant' ||\n error.error === 'invalid_token'\n );\n }\n\n const message = error instanceof Error ? error.message.toLowerCase() : '';\n return message.includes('invalid_grant') || message.includes('invalid_token') || message.includes('unauthorized');\n}\n\nfunction getLogoutScope(url: URL): LogoutScope {\n const requestedScope =\n url.searchParams.get('logout') ??\n url.searchParams.get('logout_scope') ??\n url.searchParams.get('scope');\n const normalized = requestedScope?.trim().toLowerCase();\n\n if (\n normalized === 'local' ||\n normalized === 'application' ||\n normalized === 'app' ||\n normalized === 'current-device'\n ) {\n return 'local';\n }\n\n if (\n normalized === 'all' ||\n normalized === 'all-local' ||\n normalized === 'all-devices' ||\n normalized === 'everywhere'\n ) {\n return 'all';\n }\n\n if (url.searchParams.get('local_only') === 'true' || url.searchParams.get('client_id_only') === 'true') {\n return 'local';\n }\n\n return 'identity';\n}\n\nfunction getAbsoluteRedirectUrl(request: Request, redirectUrl: string): string {\n if (/^https?:\\/\\//i.test(redirectUrl)) {\n return redirectUrl;\n }\n\n return new URL(redirectUrl, new URL(request.url).origin).toString();\n}\n\nasync function revokeTokenBestEffort(\n configuration: oidc.Configuration,\n token: string | undefined,\n tokenTypeHint: 'access_token' | 'refresh_token'\n): Promise<void> {\n if (!token) {\n return;\n }\n\n try {\n await oidc.tokenRevocation(configuration, token, {\n token_type_hint: tokenTypeHint,\n });\n } catch (error) {\n logger.warn('OIDC token revocation failed', {\n tokenTypeHint,\n error: getOAuthErrorMessage(error),\n });\n }\n}\n\nasync function revokeSessionTokensBestEffort(sessionData: SessionData): Promise<void> {\n if (!sessionData.refreshToken && !sessionData.accessToken) {\n return;\n }\n\n try {\n const configuration = await getOAuthConfig();\n await revokeTokenBestEffort(configuration, sessionData.refreshToken, 'refresh_token');\n await revokeTokenBestEffort(configuration, sessionData.accessToken, 'access_token');\n } catch (error) {\n logger.warn('Skipping OIDC token revocation because provider configuration is unavailable', {\n error: getOAuthErrorMessage(error),\n });\n }\n}\n\nasync function revokeAndDestroyAllUserSessionsBestEffort(userId: string): Promise<number> {\n const sessions = await listAuthSessionDataForUser(userId);\n\n try {\n const configuration = await getOAuthConfig();\n for (const { data } of sessions) {\n await revokeTokenBestEffort(configuration, data.refreshToken, 'refresh_token');\n await revokeTokenBestEffort(configuration, data.accessToken, 'access_token');\n }\n } catch (error) {\n logger.warn('Skipping all-session token revocation because provider configuration is unavailable', {\n error: getOAuthErrorMessage(error),\n });\n }\n\n return await destroyAuthSessionsForUser(userId);\n}\n\nfunction getClaimsFromTokenResponse(tokenResult: OidcTokenResponse): Record<string, unknown> {\n const claims = tokenResult.claims();\n\n if (claims) {\n return claims as Record<string, unknown>;\n }\n\n return getJwtClaims(tokenResult.id_token) ?? {};\n}\n\n/**\n * Get OIDC configuration with caching.\n */\nasync function getOAuthConfig(): Promise<oidc.Configuration> {\n const config = getAuthConfig();\n const now = Date.now();\n\n if (authServerCache && now - authServerCache.cachedAt < CACHE_TTL) {\n return authServerCache.configuration;\n }\n\n try {\n const configuration = await oidc.discovery(\n new URL(config.authority),\n config.clientId,\n getClientMetadata(config),\n getClientAuthentication(config),\n getDiscoveryOptions()\n );\n\n authServerCache = {\n configuration,\n cachedAt: now,\n };\n\n logger.info('OIDC discovery completed successfully', {\n issuer: configuration.serverMetadata().issuer,\n clientId: config.clientId,\n clientAuthMethod: config.clientAuthMethod,\n });\n return configuration;\n } catch (error) {\n logger.error('OIDC discovery failed', error instanceof Error ? error : undefined, {\n authority: config.authority,\n clientId: config.clientId,\n });\n throw new Error(`OIDC discovery failed: ${getOAuthErrorMessage(error)}`);\n }\n}\n\nfunction getJwks(jwksUri: string): ReturnType<typeof createRemoteJWKSet> {\n const cachedJwks = jwksCache.get(jwksUri);\n if (cachedJwks) {\n return cachedJwks;\n }\n\n const jwks = createRemoteJWKSet(new URL(jwksUri));\n jwksCache.set(jwksUri, jwks);\n return jwks;\n}\n\nasync function getLogoutTokenFromRequest(request: Request): Promise<string | null> {\n const contentType = request.headers.get('content-type') ?? '';\n\n if (contentType.includes('application/json')) {\n const body = await request.json().catch(() => null) as { logout_token?: unknown } | null;\n return typeof body?.logout_token === 'string' ? body.logout_token : null;\n }\n\n const formData = await request.formData().catch(() => null);\n const logoutToken = formData?.get('logout_token');\n return typeof logoutToken === 'string' ? logoutToken : null;\n}\n\nfunction validateBackChannelLogoutClaims(payload: Record<string, unknown>, clientId: string): {\n issuer?: string;\n subject?: string;\n sid?: string;\n} {\n const events = payload.events;\n const hasBackChannelEvent =\n events &&\n typeof events === 'object' &&\n !Array.isArray(events) &&\n 'http://schemas.openid.net/event/backchannel-logout' in events;\n\n if (!hasBackChannelEvent) {\n throw new Error('Invalid back-channel logout token: missing back-channel logout event');\n }\n\n if ('nonce' in payload) {\n throw new Error('Invalid back-channel logout token: nonce is not allowed');\n }\n\n const audience = payload.aud;\n const hasClientAudience = Array.isArray(audience)\n ? audience.includes(clientId)\n : audience === clientId;\n\n if (!hasClientAudience) {\n throw new Error('Invalid back-channel logout token: audience mismatch');\n }\n\n const subject = typeof payload.sub === 'string' ? payload.sub : undefined;\n const sid = typeof payload.sid === 'string' ? payload.sid : undefined;\n\n if (!subject && !sid) {\n throw new Error('Invalid back-channel logout token: subject or sid is required');\n }\n\n return {\n issuer: typeof payload.iss === 'string' ? payload.iss : undefined,\n subject,\n sid,\n };\n}\n\n// ============================================================================\n// Claim Extraction\n// ============================================================================\n\nconst DEFAULT_AUTH_CLAIM_MAPPING: Required<AuthClaimMapping> = {\n subject: ['sub'],\n name: ['name', 'preferred_username', 'email'],\n email: ['email'],\n givenName: ['given_name', 'givenName'],\n familyName: ['family_name', 'familyName'],\n picture: ['picture', 'avatar_url'],\n locale: ['locale'],\n zoneinfo: ['zoneinfo'],\n updatedAt: ['updated_at'],\n tenantIds: ['tenant_ids'],\n tenantRoles: ['tenant_roles'],\n permissions: ['app_perms', 'permissions', 'scope'],\n isOnboarded: ['is_onboarded'],\n};\n\nlet authClaimMapping: Required<AuthClaimMapping> = {\n ...DEFAULT_AUTH_CLAIM_MAPPING,\n};\n\nfunction normalizeClaimKeys(keys?: string[]): string[] {\n if (!Array.isArray(keys)) {\n return [];\n }\n\n return Array.from(\n new Set(\n keys\n .map((key) => key.trim())\n .filter((key) => key.length > 0)\n )\n );\n}\n\n/**\n * Configure claim mapping so different providers/backends can project their\n * own claim names onto the shared auth/session primitives.\n */\nexport function configureAuthClaimMapping(mapping: AuthClaimMapping): void {\n authClaimMapping = {\n subject: mapping.subject !== undefined ? normalizeClaimKeys(mapping.subject) : DEFAULT_AUTH_CLAIM_MAPPING.subject,\n name: mapping.name !== undefined ? normalizeClaimKeys(mapping.name) : DEFAULT_AUTH_CLAIM_MAPPING.name,\n email: mapping.email !== undefined ? normalizeClaimKeys(mapping.email) : DEFAULT_AUTH_CLAIM_MAPPING.email,\n givenName: mapping.givenName !== undefined ? normalizeClaimKeys(mapping.givenName) : DEFAULT_AUTH_CLAIM_MAPPING.givenName,\n familyName: mapping.familyName !== undefined ? normalizeClaimKeys(mapping.familyName) : DEFAULT_AUTH_CLAIM_MAPPING.familyName,\n picture: mapping.picture !== undefined ? normalizeClaimKeys(mapping.picture) : DEFAULT_AUTH_CLAIM_MAPPING.picture,\n locale: mapping.locale !== undefined ? normalizeClaimKeys(mapping.locale) : DEFAULT_AUTH_CLAIM_MAPPING.locale,\n zoneinfo: mapping.zoneinfo !== undefined ? normalizeClaimKeys(mapping.zoneinfo) : DEFAULT_AUTH_CLAIM_MAPPING.zoneinfo,\n updatedAt: mapping.updatedAt !== undefined ? normalizeClaimKeys(mapping.updatedAt) : DEFAULT_AUTH_CLAIM_MAPPING.updatedAt,\n tenantIds: mapping.tenantIds !== undefined ? normalizeClaimKeys(mapping.tenantIds) : DEFAULT_AUTH_CLAIM_MAPPING.tenantIds,\n tenantRoles: mapping.tenantRoles !== undefined ? normalizeClaimKeys(mapping.tenantRoles) : DEFAULT_AUTH_CLAIM_MAPPING.tenantRoles,\n permissions: mapping.permissions !== undefined ? normalizeClaimKeys(mapping.permissions) : DEFAULT_AUTH_CLAIM_MAPPING.permissions,\n isOnboarded: mapping.isOnboarded !== undefined ? normalizeClaimKeys(mapping.isOnboarded) : DEFAULT_AUTH_CLAIM_MAPPING.isOnboarded,\n };\n}\n\n/**\n * Reset claim mapping to the built-in defaults.\n */\nexport function resetAuthClaimMapping(): void {\n authClaimMapping = {\n ...DEFAULT_AUTH_CLAIM_MAPPING,\n };\n}\n\nfunction getConfiguredAuthClaimMapping(): Required<AuthClaimMapping> {\n return authClaimMapping;\n}\n\nfunction getFirstClaimValue(\n claims: Record<string, unknown>,\n claimKeys: string[]\n): { key?: string; value?: unknown } {\n for (const claimKey of claimKeys) {\n if (claimKey in claims) {\n return {\n key: claimKey,\n value: claims[claimKey],\n };\n }\n }\n\n return {};\n}\n\nfunction getStringClaim(claims: Record<string, unknown>, claimKeys: string[]): string | undefined {\n const { value } = getFirstClaimValue(claims, claimKeys);\n\n if (typeof value === 'string') {\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n }\n\n if (typeof value === 'number' || typeof value === 'boolean') {\n return `${value}`;\n }\n\n return undefined;\n}\n\nfunction getNumberClaim(claims: Record<string, unknown>, claimKeys: string[]): number | undefined {\n const { value } = getFirstClaimValue(claims, claimKeys);\n\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value;\n }\n\n if (typeof value === 'string') {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n }\n\n return undefined;\n}\n\nfunction getBooleanClaim(\n claims: Record<string, unknown>,\n claimKeys: string[],\n defaultValue = false\n): boolean {\n const { value } = getFirstClaimValue(claims, claimKeys);\n\n if (typeof value === 'boolean') {\n return value;\n }\n\n if (typeof value === 'string') {\n return value.toLowerCase() === 'true';\n }\n\n return defaultValue;\n}\n\n/**\n * Parse JSON safely\n */\nfunction parseJson(value: string): unknown {\n try {\n return JSON.parse(value);\n } catch {\n return null;\n }\n}\n\n/**\n * Convert value to string array\n */\nfunction toStringArray(value: unknown): string[] {\n if (!value) return [];\n\n if (Array.isArray(value)) {\n return value\n .map((item) => (typeof item === 'string' ? item.trim() : `${item}`.trim()))\n .filter((item) => item.length > 0);\n }\n\n if (typeof value === 'string') {\n const trimmed = value.trim();\n if (!trimmed) return [];\n\n if (trimmed.startsWith('[')) {\n const parsed = parseJson(trimmed);\n if (Array.isArray(parsed)) {\n return parsed\n .map((item) => (typeof item === 'string' ? item.trim() : `${item}`.trim()))\n .filter((item) => item.length > 0);\n }\n }\n\n if (trimmed.includes(',')) {\n return trimmed\n .split(',')\n .map((item) => item.trim())\n .filter((item) => item.length > 0);\n }\n\n return [trimmed];\n }\n\n return [];\n}\n\nfunction toScopeArray(value: unknown): string[] {\n if (typeof value === 'string') {\n return value\n .trim()\n .split(/\\s+/)\n .filter((item) => item.length > 0);\n }\n\n return toStringArray(value);\n}\n\n/**\n * Extract user info from ID token claims\n */\nexport function extractUserInfo(claims: Record<string, unknown>): {\n tenants: string[];\n currentTenant?: string;\n isOnboarded: boolean;\n tenantRoles: Record<string, string[]>;\n permissions: string[];\n} {\n const claimMapping = getConfiguredAuthClaimMapping();\n\n // Extract tenant IDs\n let tenants: string[] = [];\n const { value: rawTenantIds } = getFirstClaimValue(claims, claimMapping.tenantIds);\n if (rawTenantIds !== undefined) {\n if (typeof rawTenantIds === 'string' && rawTenantIds.trim().startsWith('[')) {\n const parsed = parseJson(rawTenantIds.trim());\n if (Array.isArray(parsed)) {\n tenants = parsed\n .map((id) => (typeof id === 'string' ? id.trim() : `${id}`.trim()))\n .filter((id) => id.length > 0);\n }\n } else {\n tenants = toStringArray(rawTenantIds);\n }\n }\n\n // Extract tenant roles\n const tenantRoles: Record<string, string[]> = {};\n const { value: rawRoles } = getFirstClaimValue(claims, claimMapping.tenantRoles);\n if (rawRoles !== undefined) {\n let parsedRoles: unknown = rawRoles;\n\n if (typeof rawRoles === 'string' && rawRoles.trim().startsWith('{')) {\n parsedRoles = parseJson(rawRoles.trim()) ?? rawRoles;\n }\n\n if (parsedRoles && typeof parsedRoles === 'object' && !Array.isArray(parsedRoles)) {\n for (const [tenantId, roleValue] of Object.entries(parsedRoles as Record<string, unknown>)) {\n const roleIds = toStringArray(roleValue);\n if (roleIds.length > 0) {\n tenantRoles[tenantId] = roleIds;\n }\n }\n }\n }\n\n // Extract permissions\n const permissionClaim = getFirstClaimValue(claims, claimMapping.permissions);\n const permissions =\n permissionClaim.key === 'scope'\n ? toScopeArray(permissionClaim.value)\n : toStringArray(permissionClaim.value);\n\n // Extract onboarding status\n const isOnboarded = getBooleanClaim(claims, claimMapping.isOnboarded);\n\n return { tenants, isOnboarded, tenantRoles, permissions };\n}\n\n// ============================================================================\n// Session Data Creation\n// ============================================================================\n\n/**\n * Create session data from token result\n */\nasync function createSessionData(\n tokenResult: OidcTokenResponse,\n claims: Record<string, unknown>\n): Promise<Partial<SessionData>> {\n const claimMapping = getConfiguredAuthClaimMapping();\n const baseUser: UserInfo = {\n sub: getStringClaim(claims, claimMapping.subject) || 'unknown',\n name: getStringClaim(claims, claimMapping.name) || getStringClaim(claims, claimMapping.email) || 'User',\n email: getStringClaim(claims, claimMapping.email),\n givenName: getStringClaim(claims, claimMapping.givenName),\n familyName: getStringClaim(claims, claimMapping.familyName),\n picture: getStringClaim(claims, claimMapping.picture),\n locale: getStringClaim(claims, claimMapping.locale),\n zoneinfo: getStringClaim(claims, claimMapping.zoneinfo),\n updated_at: getNumberClaim(claims, claimMapping.updatedAt),\n };\n\n const expiresAt = tokenResult.expires_in\n ? Date.now() + tokenResult.expires_in * 1000\n : undefined;\n const now = Date.now();\n const sessionState = (tokenResult as Record<string, unknown>).session_state;\n\n return {\n userId: baseUser.sub,\n accessToken: tokenResult.access_token,\n refreshToken: tokenResult.refresh_token,\n idToken: tokenResult.id_token,\n expiresAt,\n issuer: getStringClaim(claims, ['iss']),\n sid: getStringClaim(claims, ['sid']),\n sessionState: typeof sessionState === 'string' ? sessionState : undefined,\n clientId: getAuthConfig().clientId,\n user: baseUser,\n createdAt: now,\n lastActivity: now,\n };\n}\n\n// ============================================================================\n// Auth Error Handling\n// ============================================================================\n\n// Auth error cookie names - unique per application to prevent conflicts\nconst AUTH_ERROR_COOKIE = `${AUTH_ERROR_COOKIE_PREFIX}_auth_error`;\nconst AUTH_ERROR_DESC_COOKIE = `${AUTH_ERROR_COOKIE_PREFIX}_auth_error_desc`;\nconst OAUTH_STATE_COOKIE = `${AUTH_ERROR_COOKIE_PREFIX}_oauth_state_id`;\n\n/**\n * Check if there's an auth error in cookies\n * \n * This should be called by apps (especially those without landing pages like Dashboard)\n * BEFORE starting OAuth flow to prevent redirect loops.\n * \n * @example\n * ```ts\n * // In dashboard's index route loader\n * const authError = getAuthError(request);\n * if (authError) {\n * return { authError }; // Show error to user\n * }\n * // No error, proceed with session check\n * const user = await getUser(request);\n * if (!user) {\n * return login(request);\n * }\n * ```\n */\nexport function getAuthError(request: Request): AuthError | null {\n const cookies = request.headers.get('Cookie');\n if (!cookies) return null;\n \n // Parse auth_error cookie (app-specific)\n const errorRegex = new RegExp(`${AUTH_ERROR_COOKIE}=([^;]+)`);\n const errorMatch = cookies.match(errorRegex);\n if (!errorMatch) return null;\n \n const error = decodeURIComponent(errorMatch[1]);\n if (!error) return null;\n \n // Parse auth_error_description cookie (app-specific)\n const descRegex = new RegExp(`${AUTH_ERROR_DESC_COOKIE}=([^;]+)`);\n const descMatch = cookies.match(descRegex);\n const description = descMatch ? decodeURIComponent(descMatch[1]) : undefined;\n \n return { error, description };\n}\n\n/**\n * Clear auth error cookies\n * \n * Call this after displaying the error to the user\n */\nexport function clearAuthErrorHeaders(): Headers {\n const headers = new Headers();\n headers.append('Set-Cookie', `${AUTH_ERROR_COOKIE}=; Path=/; Max-Age=0`);\n headers.append('Set-Cookie', `${AUTH_ERROR_DESC_COOKIE}=; Path=/; Max-Age=0`);\n return headers;\n}\n\n/**\n * Check if request has auth error (quick check without parsing)\n */\nexport function hasAuthError(request: Request): boolean {\n const cookies = request.headers.get('Cookie');\n return cookies?.includes(`${AUTH_ERROR_COOKIE}=`) ?? false;\n}\n\n// ============================================================================\n// Auth Functions\n// ============================================================================\n\n/**\n * Initiate OAuth login flow\n * \n * @param request - The incoming request\n * @param returnUrlOrOptions - Either a return URL string or LoginOptions object\n */\nexport async function login(\n request: Request, \n returnUrlOrOptions?: string | LoginOptions\n): Promise<Response> {\n try {\n const config = getAuthConfig();\n \n // Parse options\n const options: LoginOptions = typeof returnUrlOrOptions === 'string' \n ? { returnUrl: returnUrlOrOptions }\n : returnUrlOrOptions || {};\n\n // Check for auth_error cookie to prevent redirect loops\n const cookies = request.headers.get('Cookie');\n const hasError = cookies?.includes(`${AUTH_ERROR_COOKIE}=`);\n if (hasError) {\n logger.warn('Auth error cookie detected, preventing redirect loop');\n // Clear the error cookie and redirect to home\n const headers = new Headers();\n headers.append('Set-Cookie', `${AUTH_ERROR_COOKIE}=; Path=/; Max-Age=0`);\n // Use redirectUri to get correct base URL with port\n const redirectUri = new URL(config.redirectUri);\n const baseUrl = redirectUri.origin;\n return createRedirectResponse(baseUrl, { headers });\n }\n\n const oidcConfig = await getOAuthConfig();\n\n // Generate PKCE challenge\n const codeVerifier = oidc.randomPKCECodeVerifier();\n const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);\n\n const extraAuthParams = sanitizeExtraAuthorizationParameters(options.extraAuthParams, options);\n\n // Generate secure state and nonce. The state may carry a non-secret UI\n // hint for Keycloak themes, but callback validation still compares the\n // exact value stored server-side.\n const state = buildAuthorizationState(oidc.randomState(), options.publicStateContext);\n const nonce = oidc.randomNonce();\n\n // Create OAuth state object\n const oauthState: OAuthState = {\n state,\n codeVerifier,\n nonce,\n returnUrl: options.returnUrl,\n kcAction: options.kcAction,\n createdAt: Date.now(),\n };\n\n logger.info('Creating OAuth state', {\n returnUrl: options.returnUrl,\n prompt: options.prompt,\n kcAction: options.kcAction,\n extraAuthParamKeys: Object.keys(extraAuthParams),\n });\n\n // Store OAuth state in Redis\n const stateId = await createOAuthState(oauthState);\n\n const authorizationParameters: Record<string, string> = {\n redirect_uri: config.redirectUri,\n scope: config.scope,\n state,\n nonce,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n ...extraAuthParams,\n };\n\n if (options.prompt) {\n authorizationParameters.prompt = options.prompt;\n }\n\n if (options.kcAction) {\n authorizationParameters.kc_action = options.kcAction;\n }\n\n const authorizationUrl = oidc.buildAuthorizationUrl(oidcConfig, authorizationParameters);\n\n // Store stateId in cookie for callback (app-specific to prevent conflicts)\n const headers = new Headers();\n headers.append('Set-Cookie', `${OAUTH_STATE_COOKIE}=${stateId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`);\n\n logger.info('OIDC authorization flow initiated', {\n clientId: config.clientId,\n redirectUri: config.redirectUri,\n scope: config.scope,\n authorizationEndpoint: oidcConfig.serverMetadata().authorization_endpoint,\n stateId,\n prompt: options.prompt,\n kcAction: options.kcAction,\n });\n\n return createRedirectResponse(authorizationUrl.toString(), { headers });\n } catch (error) {\n logger.error('Failed to initiate OAuth login', error instanceof Error ? error : undefined);\n throw new Error('Login failed');\n }\n}\n\n/**\n * Handle OAuth callback\n * \n * Handles OAuth authorization response including success and error cases.\n * \n * Error Handling:\n * - `access_denied`: User doesn't have access to this application\n * - For apps with landing page: Redirect home with error cookie\n * - For apps without landing page: Redirect home with error cookie (must handle specially)\n * - `login_required`: No active identity session\n * - Redirect to home to start fresh login\n */\nexport async function handleCallback(request: Request): Promise<Response> {\n try {\n const config = getAuthConfig();\n const hasLandingPage = getEffectiveHasLandingPage(config);\n const url = new URL(request.url);\n const code = url.searchParams.get('code');\n const state = url.searchParams.get('state');\n const error = url.searchParams.get('error');\n const errorDescription = url.searchParams.get('error_description');\n const kcAction = url.searchParams.get('kc_action');\n const kcActionStatus = url.searchParams.get('kc_action_status');\n\n logger.info('OIDC callback received', {\n hasCode: !!code,\n hasState: !!state,\n error,\n errorDescription,\n kcAction,\n kcActionStatus,\n clientId: config.clientId,\n redirectUri: config.redirectUri\n });\n\n // Handle OAuth errors\n if (error) {\n if (kcActionStatus && state) {\n const cookies = request.headers.get('Cookie');\n const oauthStateRegex = new RegExp(`${OAUTH_STATE_COOKIE}=([^;]+)`);\n const cookieMatch = cookies?.match(oauthStateRegex);\n const stateId = cookieMatch?.[1];\n\n if (stateId) {\n const oauthState = await getOAuthState(stateId);\n const stateAge = oauthState ? Date.now() - oauthState.createdAt : Number.POSITIVE_INFINITY;\n\n if (oauthState && oauthState.state === state && stateAge <= 10 * 60 * 1000) {\n await deleteOAuthState(stateId);\n const headers = new Headers();\n headers.append('Set-Cookie', `${OAUTH_STATE_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`);\n\n const redirectUrl = appendApplicationInitiatedActionResult(\n oauthState.returnUrl || '/',\n kcAction || oauthState.kcAction,\n kcActionStatus\n );\n\n logger.info('Application initiated action returned with OAuth error', {\n error,\n errorDescription,\n kcAction: kcAction || oauthState.kcAction,\n kcActionStatus,\n });\n\n return createRedirectResponse(redirectUrl, { headers });\n }\n }\n }\n\n logger.error('OAuth callback error', undefined, { \n error, \n errorDescription,\n clientId: config.clientId,\n applicationType: config.applicationType,\n hasLandingPage\n });\n \n // Handle access_denied specially - user doesn't have access to this app\n // This can happen when identity cookie is valid but user has no access to this client\n if (error === 'access_denied') {\n logger.warn('Access denied for this application', { clientId: config.clientId });\n // Clear any existing session and redirect to home without triggering new login\n const headers = await destroyAuthSession(request);\n // Set error cookies to:\n // 1. Prevent redirect loop (auth_error)\n // 2. Allow UI to show appropriate message (auth_error_type, auth_error_description)\n headers.append(\n 'Set-Cookie',\n `${AUTH_ERROR_COOKIE}=access_denied; Path=/; HttpOnly; SameSite=Lax; Max-Age=300`\n );\n headers.append(\n 'Set-Cookie',\n `${AUTH_ERROR_DESC_COOKIE}=${encodeURIComponent(errorDescription || 'You do not have access to this application.')}; Path=/; SameSite=Lax; Max-Age=300`\n );\n // Use redirectUri to get the correct base URL with port\n // request.url.origin can lose the port in some cases\n const redirectUri = new URL(config.redirectUri);\n const baseUrl = redirectUri.origin;\n logger.info('Redirecting to /auth/login to show access_denied error', { \n hasLandingPage,\n baseUrl,\n configRedirectUri: config.redirectUri\n });\n // Redirect to /auth/login so user immediately sees the access denied page\n // instead of landing page (which doesn't show the error)\n return createRedirectResponse(`${baseUrl}/auth/login`, { headers });\n }\n \n // Handle login_required - session expired or user not logged in\n if (error === 'login_required') {\n logger.info('Login required, user session may have expired');\n const headers = await destroyAuthSession(request);\n // Use redirectUri to get correct base URL with port\n const redirectUri = new URL(config.redirectUri);\n const baseUrl = redirectUri.origin;\n return createRedirectResponse(baseUrl, { headers });\n }\n \n // Handle other errors\n const headers = await destroyAuthSession(request);\n headers.append(\n 'Set-Cookie',\n `${AUTH_ERROR_COOKIE}=${encodeURIComponent(error)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=300`\n );\n if (errorDescription) {\n headers.append(\n 'Set-Cookie',\n `${AUTH_ERROR_DESC_COOKIE}=${encodeURIComponent(errorDescription)}; Path=/; SameSite=Lax; Max-Age=300`\n );\n }\n // Use redirectUri to get correct base URL with port\n const redirectUri = new URL(config.redirectUri);\n const baseUrl = redirectUri.origin;\n return createRedirectResponse(baseUrl, { headers });\n }\n\n // Get stateId from cookie (app-specific)\n const cookies = request.headers.get('Cookie');\n const oauthStateRegex = new RegExp(`${OAUTH_STATE_COOKIE}=([^;]+)`);\n const cookieMatch = cookies?.match(oauthStateRegex);\n const stateId = cookieMatch?.[1];\n\n if (!stateId) {\n throw new Error('OAuth state ID not found in cookie');\n }\n\n // Retrieve OAuth state from Redis\n const oauthState = await getOAuthState(stateId);\n\n if (!oauthState) {\n throw new Error('No OAuth state found - state may have expired');\n }\n\n if (!state) {\n throw new Error('Missing required callback state parameter');\n }\n\n if (oauthState.state !== state) {\n throw new Error(`OAuth state mismatch`);\n }\n\n // Check state age (10 minutes max)\n const stateAge = Date.now() - oauthState.createdAt;\n if (stateAge > 10 * 60 * 1000) {\n throw new Error('OAuth state expired');\n }\n\n if (!code) {\n if (kcActionStatus) {\n await deleteOAuthState(stateId);\n const headers = new Headers();\n headers.append('Set-Cookie', `${OAUTH_STATE_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`);\n\n const redirectUrl = appendApplicationInitiatedActionResult(\n oauthState.returnUrl || '/',\n kcAction || oauthState.kcAction,\n kcActionStatus\n );\n\n logger.info('Application initiated action returned without authorization code', {\n kcAction: kcAction || oauthState.kcAction,\n kcActionStatus,\n });\n\n return createRedirectResponse(redirectUrl, { headers });\n }\n\n throw new Error('Missing required callback code parameter');\n }\n\n logger.info('Initiating token exchange');\n\n const oidcConfig = await getOAuthConfig();\n const tokenResult = await oidc.authorizationCodeGrant(oidcConfig, url, {\n pkceCodeVerifier: oauthState.codeVerifier,\n expectedState: oauthState.state,\n expectedNonce: oauthState.nonce,\n idTokenExpected: config.scope.split(/\\s+/).includes('openid'),\n });\n\n logger.info('Token exchange completed successfully');\n\n const claims = getClaimsFromTokenResponse(tokenResult);\n\n // Create session data\n const newSessionData = await createSessionData(tokenResult, claims);\n\n // Create new session\n const sessionHeaders = await createAuthSession(request, newSessionData);\n\n // Clean up OAuth state from Redis\n await deleteOAuthState(stateId);\n\n // Combine headers and clear OAuth state cookie (app-specific)\n const headers = new Headers(sessionHeaders);\n headers.append('Set-Cookie', `${OAUTH_STATE_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`);\n\n logger.info('OAuth callback processed successfully');\n\n // Redirect to return URL or default location\n const redirectUrl = appendApplicationInitiatedActionResult(\n oauthState.returnUrl || '/',\n kcAction || oauthState.kcAction,\n kcActionStatus\n );\n return createRedirectResponse(redirectUrl, { headers });\n } catch (error) {\n logger.error('OAuth callback failed', error instanceof Error ? error : undefined);\n\n // Try to clean up OAuth state on error\n try {\n const cookies = request.headers.get('Cookie');\n const oauthStateRegex = new RegExp(`${OAUTH_STATE_COOKIE}=([^;]+)`);\n const cookieMatch = cookies?.match(oauthStateRegex);\n const stateId = cookieMatch?.[1];\n if (stateId) {\n await deleteOAuthState(stateId);\n }\n } catch {\n // Ignore cleanup errors\n }\n\n await destroyAuthSession(request);\n throw new Error('Authentication failed');\n }\n}\n\n/**\n * Logout user.\n *\n * Default logout follows OIDC RP-Initiated Logout and ends the identity\n * provider session. Automatic/session-expiry cleanup can request\n * `/auth/logout?logout=local` to clear only this app's Redis session without\n * bouncing the browser through the identity provider or revoking SSO tokens.\n */\nexport async function logout(request: Request): Promise<Response> {\n try {\n const config = getAuthConfig();\n const url = new URL(request.url);\n \n const logoutScope = getLogoutScope(url);\n const returnUrl = sanitizeLogoutReturnUrl(url.searchParams.get('returnUrl'), request);\n \n logger.info('Logout initiated', { \n clientId: config.clientId,\n logoutScope,\n });\n \n const sessionData = await getAuthSession(request);\n const idToken = sessionData.idToken;\n\n if (logoutScope === 'local') {\n // Local logout is RP-only cleanup. Tokens are server-side and become\n // unreachable once the Redis session is destroyed; revoking the refresh\n // token can also terminate the Keycloak SSO session for this user.\n const headers = await destroyAuthSession(request);\n const redirectUrl = returnUrl || '/';\n const absoluteRedirectUrl = getAbsoluteRedirectUrl(request, redirectUrl);\n\n headers.append('Set-Cookie', 'logout_return_url=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0');\n logger.info('Local application logout completed', { redirectUrl: absoluteRedirectUrl });\n return createRedirectResponse(absoluteRedirectUrl, { headers });\n }\n\n const sessionHeaders = new Headers();\n\n if (logoutScope === 'all' && sessionData.userId) {\n await revokeAndDestroyAllUserSessionsBestEffort(sessionData.userId);\n sessionHeaders.append('Set-Cookie', 'logout_return_url=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0');\n const expiredHeaders = await destroyAuthSession(request);\n expiredHeaders.forEach((value, key) => sessionHeaders.append(key, value));\n } else {\n await revokeSessionTokensBestEffort(sessionData);\n const expiredHeaders = await destroyAuthSession(request);\n expiredHeaders.forEach((value, key) => sessionHeaders.append(key, value));\n }\n\n const headers = new Headers(sessionHeaders);\n\n // Store returnUrl in cookie if provided for the post-logout landing redirect.\n if (returnUrl) {\n headers.append(\n 'Set-Cookie',\n `logout_return_url=${encodeURIComponent(returnUrl)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=300`\n );\n } else {\n headers.append('Set-Cookie', 'logout_return_url=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0');\n }\n\n try {\n const oidcConfig = await getOAuthConfig();\n const serverMetadata = oidcConfig.serverMetadata();\n const logoutParameters: Record<string, string> = {};\n const expectedIssuer = serverMetadata.issuer ?? config.authority;\n\n if (idToken && canUseIdTokenHint(idToken, expectedIssuer)) {\n logoutParameters.id_token_hint = idToken;\n } else if (idToken) {\n logger.warn('Skipping id_token_hint because the session token issuer does not match the active OIDC provider', {\n tokenIssuer: getJwtIssuer(idToken),\n expectedIssuer: normalizeIssuer(expectedIssuer),\n clientId: config.clientId,\n });\n }\n\n if (config.postLogoutRedirectUri) {\n logoutParameters.post_logout_redirect_uri = config.postLogoutRedirectUri;\n }\n\n const endSessionUrl = oidc.buildEndSessionUrl(oidcConfig, logoutParameters);\n logger.info('RP-Initiated Logout redirect created', {\n endSessionEndpoint: serverMetadata.end_session_endpoint,\n hasIdTokenHint: Boolean(logoutParameters.id_token_hint),\n });\n\n return createRedirectResponse(endSessionUrl.toString(), { headers });\n } catch (error) {\n logger.warn('OIDC end_session endpoint unavailable, falling back to local redirect', {\n error: getOAuthErrorMessage(error),\n });\n }\n\n // Fallback redirect\n const registeredPath = config.postLogoutRedirectUri || '/';\n const finalRedirectUrl = getAbsoluteRedirectUrl(request, registeredPath);\n\n logger.info('Logout completed', { redirectUrl: finalRedirectUrl });\n return createRedirectResponse(finalRedirectUrl, { headers });\n } catch (error) {\n logger.error('Logout failed', error instanceof Error ? error : undefined);\n const headers = await destroyAuthSession(request);\n return createRedirectResponse('/', { headers });\n }\n}\n\n/**\n * Handle an OIDC Back-Channel Logout request from Keycloak or another OP.\n *\n * Configure the client Backchannel logout URL to this endpoint. The logout\n * token is signature-verified against the provider JWKS, issuer/audience are\n * checked, and all matching local Redis sessions are destroyed by `sid` first,\n * falling back to `sub` when the OP omits a session id.\n */\nexport async function handleBackChannelLogout(request: Request): Promise<Response> {\n if (request.method !== 'POST') {\n return Response.json({ error: 'method_not_allowed' }, { status: 405 });\n }\n\n try {\n const config = getAuthConfig();\n const oidcConfig = await getOAuthConfig();\n const serverMetadata = oidcConfig.serverMetadata();\n const logoutToken = await getLogoutTokenFromRequest(request);\n\n if (!logoutToken) {\n return Response.json({ error: 'missing_logout_token' }, { status: 400 });\n }\n\n if (!serverMetadata.jwks_uri) {\n throw new Error('OIDC provider does not expose jwks_uri');\n }\n\n const { payload } = await jwtVerify(logoutToken, getJwks(serverMetadata.jwks_uri), {\n issuer: serverMetadata.issuer ?? config.authority,\n audience: config.clientId,\n clockTolerance: 60,\n });\n\n const claims = validateBackChannelLogoutClaims(payload as Record<string, unknown>, config.clientId);\n const destroyedSessions = await destroyAuthSessionsByIdentitySession({\n sid: claims.sid,\n userId: claims.subject,\n });\n const result: BackChannelLogoutResult = {\n destroyedSessions,\n issuer: claims.issuer,\n subject: claims.subject,\n sid: claims.sid,\n };\n\n logger.info('Back-channel logout processed', { ...result });\n return Response.json({ success: true, ...result });\n } catch (error) {\n logger.error('Back-channel logout failed', error instanceof Error ? error : undefined);\n return Response.json(\n { error: 'invalid_logout_token', error_description: getOAuthErrorMessage(error) },\n { status: 400 }\n );\n }\n}\n\n/**\n * Handle OIDC Front-Channel Logout iframe/browser requests.\n *\n * Front-channel logout cannot carry a signed logout token, so this validates the\n * issuer and applies local cleanup only when a `sid` is present.\n */\nexport async function handleFrontChannelLogout(request: Request): Promise<Response> {\n try {\n const config = getAuthConfig();\n const oidcConfig = await getOAuthConfig();\n const serverMetadata = oidcConfig.serverMetadata();\n const url = new URL(request.url);\n const issuer = url.searchParams.get('iss');\n const sid = url.searchParams.get('sid');\n const expectedIssuer = normalizeIssuer(serverMetadata.issuer ?? config.authority);\n\n if (!issuer || normalizeIssuer(issuer) !== expectedIssuer) {\n return new Response(null, { status: 204 });\n }\n\n const destroyedSessions = sid ? await destroyAuthSessionsBySid(sid) : 0;\n const result: FrontChannelLogoutResult = {\n destroyedSessions,\n issuer,\n sid: sid ?? undefined,\n };\n\n logger.info('Front-channel logout processed', { ...result });\n return new Response(null, {\n status: 204,\n headers: {\n 'Cache-Control': 'no-store',\n },\n });\n } catch (error) {\n logger.warn('Front-channel logout ignored', {\n error: getOAuthErrorMessage(error),\n });\n\n return new Response(null, { status: 204 });\n }\n}\n\n/**\n * Get current user from session\n */\nexport async function getUser(request: Request): Promise<UserInfo | null> {\n try {\n const sessionData = await getAuthSession(request);\n return sessionData.user || null;\n } catch (error) {\n logger.error('Failed to get user', error instanceof Error ? error : undefined);\n return null;\n }\n}\n\n/**\n * Require authenticated user (throws redirect to login if not authenticated)\n */\nexport async function requireAuth(request: Request): Promise<UserInfo> {\n const user = await getUser(request);\n\n if (!user) {\n const url = new URL(request.url);\n const returnUrl = `${url.pathname}${url.search}`;\n logger.info('No user found, starting OAuth flow', { returnUrl });\n throw await login(request, returnUrl);\n }\n\n return user;\n}\n\n/**\n * Get access token from session\n */\nexport async function getAccessToken(request: Request): Promise<string | null> {\n try {\n const sessionData = await getAuthSession(request);\n return sessionData.accessToken || null;\n } catch (error) {\n logger.error('Failed to get access token', error instanceof Error ? error : undefined);\n return null;\n }\n}\n\n/**\n * Refresh tokens\n */\nexport async function refreshTokens(\n request: Request,\n refreshToken?: string\n): Promise<TokenRefreshResult> {\n try {\n const sessionData = await getAuthSession(request);\n const tokenToRefresh = refreshToken || sessionData.refreshToken;\n\n if (!tokenToRefresh) {\n logger.warn('No refresh token available');\n return { success: false, error: 'No refresh token available', shouldLogout: true };\n }\n\n logger.info('Initiating token refresh');\n\n const oidcConfig = await getOAuthConfig();\n const refreshResult = await oidc.refreshTokenGrant(oidcConfig, tokenToRefresh);\n\n logger.info('Token refresh successful');\n\n const newRefreshToken = refreshResult.refresh_token || tokenToRefresh;\n const expiresAt = refreshResult.expires_in\n ? Date.now() + refreshResult.expires_in * 1000\n : sessionData.expiresAt;\n\n // Update session\n await updateAuthSession(request, {\n accessToken: refreshResult.access_token,\n refreshToken: newRefreshToken,\n idToken: refreshResult.id_token,\n expiresAt,\n lastActivity: Date.now(),\n });\n\n return {\n success: true,\n tokens: {\n access_token: refreshResult.access_token,\n refresh_token: newRefreshToken,\n id_token: refreshResult.id_token,\n expires_in: refreshResult.expires_in,\n },\n };\n } catch (error) {\n logger.error('Token refresh failed with exception', error instanceof Error ? error : undefined);\n\n return {\n success: false,\n error: getOAuthErrorMessage(error),\n shouldLogout: shouldLogoutAfterOAuthError(error),\n };\n }\n}\n\n// Re-export session validation\nexport { isSessionValid };\n\n/**\n * Clear auth server cache (useful for testing)\n */\nexport function clearAuthServerCache(): void {\n authServerCache = null;\n jwksCache.clear();\n}\n"],"mappings":";;;;;;;;;AAYA,SAAgB,uBACdA,UACAC,OAA6B,CAAE,GACrB;CACV,MAAM,UAAU,IAAI,QAAQ,KAAK;AACjC,SAAQ,IAAI,YAAY,SAAS;AAEjC,QAAO,IAAI,SAAS,MAAM;EACxB,QAAQ,KAAK,UAAU;EACvB;CACD;AACF;;;;ACrBD,MAAM,yCAAyC,IAAI,IAAI;CACrD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD;AAED,MAAM,8CAA8C,IAAI,IAAI;CAC1D;CACA;CACA;CACA;AACD;AAED,SAAS,qCACPC,KACAC,cACAC,iBACS;CACT,MAAM,aAAa,IAAI,aAAa;AACpC,QAAO,aAAa,IAAI,WAAW,IAAI,gBAAgB,KAAK,CAAC,WAAW,WAAW,WAAW,OAAO,CAAC;AACvG;AAED,SAAgB,qCACdC,QACAC,UAAgF,CAAE,GAC1D;AACxB,MAAK,OACH,QAAO,CAAE;CAGX,MAAM,eAAe,IAAI,IAAI,CAC3B,GAAG,6CACH,GAAG,CAAC,QAAQ,uBAAuB,CAAE,GAAE,IAAI,CAAC,SAAS,KAAK,MAAM,CAAC,aAAa,CAAC,CAAC,OAAO,QAAQ,AAChG;CACD,MAAM,kBAAkB,CAAC,QAAQ,0BAA0B,CAAE,GAC1D,IAAI,CAAC,WAAW,OAAO,MAAM,CAAC,aAAa,CAAC,CAC5C,OAAO,QAAQ;CAClB,MAAMC,YAAoC,CAAE;AAE5C,MAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,QAAQ,OAAO,EAAE;EACjD,MAAM,gBAAgB,IAAI,MAAM;AAChC,MACE,cAAc,WAAW,KACzB,uCAAuC,IAAI,cAAc,aAAa,CAAC,KACtE,qCAAqC,eAAe,cAAc,gBAAgB,IACnF,UAAU,QACV,iBAEA;EAGF,MAAM,kBAAkB,OAAO,MAAM,CAAC,MAAM;AAC5C,MAAI,gBAAgB,WAAW,EAC7B;AAGF,YAAU,iBAAiB;CAC5B;AAED,QAAO;AACR;AAED,SAAgB,yCACdC,SAC+B;AAC/B,MAAK,QACH,QAAO;CAGT,MAAMC,aAAqC,CAAE;AAE7C,MAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,QAAQ,QAAQ,EAAE;EAClD,MAAM,gBAAgB,IAAI,MAAM;AAChC,MACE,cAAc,WAAW,KACzB,uCAAuC,IAAI,cAAc,aAAa,CAAC,IACvE,UAAU,QACV,iBAEA;EAGF,MAAM,kBAAkB,OAAO,MAAM,CAAC,MAAM;AAC5C,MAAI,gBAAgB,WAAW,EAC7B;AAGF,aAAW,iBAAiB;CAC7B;AAED,QAAO,OAAO,KAAK,WAAW,CAAC,SAAS,IAAI,aAAa;AAC1D;AAED,SAAgB,wBACdC,WACAC,oBACQ;CACR,MAAM,oBAAoB,yCAAyC,mBAAmB;AACtF,MAAK,kBACH,QAAO;AAGT,SAAQ,EAAE,UAAU,GAAG,OAAO,KAAK,KAAK,UAAU,kBAAkB,EAAE,OAAO,CAAC,SAAS,YAAY,CAAC;AACrG;;;;;;;ACrDD,SAAS,2BAA2BC,QAA6B;AAE/D,KAAI,OAAO,0BACT,QAAO,OAAO;AAIhB,SAAQ,OAAO,iBAAf;EACE,KAAK;EACL,KAAK,YACH,QAAO;EACT,KAAK;EACL,KAAK,aACH,QAAO;EACT,QACE,QAAO;CACV;AACF;;;;AAKD,SAAS,mBAA+B;CACtC,MAAM,kBAAkB;EACtB,WAAW,QAAQ,IAAI;EACvB,UAAU,QAAQ,IAAI;EACtB,aAAa,QAAQ,IAAI;CAC1B;AAED,MAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,QAAQ,gBAAgB,CACxD,MAAK,MACH,OAAM,IAAI,OAAO,8CAA8C,IAAI,aAAa,CAAC;CAKrF,MAAM,aAAa,QAAQ,IAAI,uBAAuB,aAAa;CACnE,IAAIC,kBAAmC;AACvC,KAAI,eAAe,qBAAqB,eAAe,kBACrD,mBAAkB;UACT,eAAe,kBAAkB,eAAe,eACzD,mBAAkB;UACT,eAAe,YACxB,mBAAkB;UACT,eAAe,gBAAgB,eAAe,aACvD,mBAAkB;CAGpB,MAAM,mBAAmB,0BACvB,QAAQ,IAAI,yBACZ,QAAQ,IAAI,qBAAqB,uBAAuB,OACzD;CAED,MAAM,iBAAiB,QAAQ,IAAI,mCAC/B,QAAQ,IAAI,0BAA0B;AAG1C,QAAO;EACL,WAAW,gBAAgB;EAC3B,UAAU,gBAAgB;EAC1B,cAAc,QAAQ,IAAI;EAC1B;EACA,aAAa,gBAAgB;EAC7B,OAAO,QAAQ,IAAI,cAAc;EACjC,uBAAuB,QAAQ,IAAI;EACnC;EACA;CACD;AACF;AAED,SAAS,0BACPC,OACAC,UACsB;CACtB,MAAM,aAAa,OAAO,MAAM,CAAC,aAAa;AAE9C,SAAQ,YAAR;EACE;EACA,KAAK,GACH,QAAO;EACT,KAAK,OACH,QAAO;EACT,KAAK;EACL,KAAK,OACH,QAAO;EACT,KAAK;EACL,KAAK,QACH,QAAO;EACT,QACE,OAAM,IAAI,OAAO,uCAAuC,MAAM;CACjE;AACF;AAED,IAAIC,cAAiC;AAErC,SAAS,gBAA4B;AACnC,MAAK,YACH,eAAc,kBAAkB;AAElC,QAAO;AACR;AAMD,IAAIC,kBAGO;AAEX,MAAM,YAAY,KAAK,KAAK;AAC5B,MAAM,YAAY,IAAI;AAEtB,SAAS,gBAAgBH,OAA0C;CACjE,MAAM,aAAa,OAAO,MAAM,CAAC,QAAQ,QAAQ,GAAG;AACpD,QAAO,cAAc,WAAW,SAAS,IAAI,aAAa;AAC3D;AAED,SAAS,aAAaI,OAA2D;AAC/E,MAAK,MACH,QAAO;AAGT,KAAI;EACF,MAAM,GAAG,QAAQ,GAAG,MAAM,MAAM,IAAI;AACpC,OAAK,QACH,QAAO;AAGT,SAAO,KAAK,MAAM,OAAO,KAAK,SAAS,YAAY,CAAC,SAAS,OAAO,CAAC;CACtE,QAAO;AACN,SAAO;CACR;AACF;AAED,SAAS,aAAaA,OAA0C;CAC9D,MAAM,SAAS,aAAa,MAAM,EAAE;AACpC,eAAc,WAAW,WAAW,gBAAgB,OAAO,GAAG;AAC/D;AAED,SAAS,kBAAkBC,SAA6BC,gBAAiC;AACvF,MAAK,QACH,QAAO;CAGT,MAAM,cAAc,aAAa,QAAQ;AACzC,QAAO,gBAAgB,QAAQ,gBAAgB,gBAAgB,eAAe;AAC/E;AAED,SAAS,wBAAwBC,WAA0BC,SAAiC;AAC1F,MAAK,UACH,QAAO;AAGT,KAAI;EACF,MAAM,aAAa,IAAI,IAAI,QAAQ;EACnC,MAAM,mBAAmB,mBAAmB,UAAU;AAEtD,MAAI,iBAAiB,WAAW,IAAI,KAAK,iBAAiB,WAAW,KAAK,CACxE,QAAO,eAAe,iBAAiB,GAAG,OAAO;EAGnD,MAAM,eAAe,IAAI,IAAI;AAC7B,MAAI,aAAa,WAAW,WAAW,OACrC,QAAO;EAGT,MAAM,kBAAkB,EAAE,aAAa,SAAS,EAAE,aAAa,OAAO,EAAE,aAAa,KAAK;AAC1F,SAAO,eAAe,eAAe,GAAG,OAAO;CAChD,QAAO;AACN,SAAO;CACR;AACF;AAED,SAAS,uCACPC,WACAC,QACAC,QACQ;AACR,MAAK,WAAW,OACd,QAAO;AAGT,KAAI;EACF,MAAM,aAAa,UAAU,WAAW,IAAI,KAAK,UAAU,WAAW,KAAK;EAC3E,MAAM,MAAM,IAAI,IAAI,WAAW,aAAa;AAE5C,MAAI,OACF,KAAI,aAAa,IAAI,aAAa,OAAO;AAG3C,MAAI,OACF,KAAI,aAAa,IAAI,oBAAoB,OAAO;AAGlD,SAAO,cAAc,EAAE,IAAI,SAAS,EAAE,IAAI,OAAO,EAAE,IAAI,KAAK,IAAI,IAAI,UAAU;CAC/E,QAAO;EACN,MAAM,SAAS,IAAI;AACnB,MAAI,OAAQ,QAAO,IAAI,aAAa,OAAO;AAC3C,MAAI,OAAQ,QAAO,IAAI,oBAAoB,OAAO;EAClD,MAAM,YAAY,UAAU,SAAS,IAAI,GAAG,MAAM;AAClD,UAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,UAAU,CAAC;CACrD;AACF;AAED,SAAS,eAAeC,MAAuB;CAC7C,MAAM,aAAa,KAAK,MAAM,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG,QAAQ,QAAQ,GAAG,IAAI;AAC3E,QAAO,eAAe,kBAAkB,eAAe;AACxD;AAED,SAAS,sBAAsBC,WAA4B;AACzD,KAAI;EACF,MAAM,SAAS,IAAI,IAAI;AACvB,SACE,OAAO,aAAa,WACpB;GAAC;GAAa;GAAa;GAAO;EAAQ,EAAC,SAAS,OAAO,SAAS;CAEvE,QAAO;AACN,SAAO;CACR;AACF;AAED,SAAS,gCAAgCA,WAA4B;AACnE,KAAI,QAAQ,IAAI,aAAa,aAC3B,QAAO;AAGT,KAAI,QAAQ,IAAI,iCAAiC,OAC/C,QAAO;AAGT,QAAO,sBAAsB,UAAU;AACxC;AAED,SAAS,sBAAoD;CAC3D,MAAM,SAAS,eAAe;AAE9B,QAAO,gCAAgC,OAAO,UAAU,GACpD;EAAE,WAAW;EAAQ,SAAS,CAAC,KAAK,qBAAsB;CAAE,IAC5D,EAAE,WAAW,OAAQ;AAC1B;AAED,SAAS,kBAAkBf,QAAwC;AACjE,QAAO,OAAO,qBAAqB,kBAAqB,OAAO;AAChE;AAED,SAAS,wBAAwBA,QAAqC;AACpE,SAAQ,OAAO,kBAAf;EACE,KAAK;AACH,QAAK,OAAO,aACV,OAAM,IAAI,MAAM;AAGlB,UAAO,KAAK,kBAAkB,OAAO,aAAa;EACpD,KAAK;AACH,QAAK,OAAO,aACV,OAAM,IAAI,MAAM;AAGlB,UAAO,KAAK,iBAAiB,OAAO,aAAa;EACnD,KAAK;EACL,QACE,QAAO,KAAK,MAAM;CACrB;AACF;AAED,SAAS,qBAAqBgB,OAAwB;AACpD,KAAI,iBAAiB,KAAK,kBACxB,QAAO,MAAM,qBAAqB,MAAM,SAAS,MAAM;AAGzD,KAAI,iBAAiB,MACnB,QAAO,MAAM;AAGf,QAAO;AACR;AAED,SAAS,4BAA4BA,OAAyB;AAC5D,KAAI,iBAAiB,KAAK,kBACxB,QACE,MAAM,WAAW,OACjB,MAAM,WAAW,OACjB,MAAM,UAAU,mBAChB,MAAM,UAAU;CAIpB,MAAM,UAAU,iBAAiB,QAAQ,MAAM,QAAQ,aAAa,GAAG;AACvE,QAAO,QAAQ,SAAS,gBAAgB,IAAI,QAAQ,SAAS,gBAAgB,IAAI,QAAQ,SAAS,eAAe;AAClH;AAED,SAAS,eAAeC,KAAuB;CAC7C,MAAM,iBACJ,IAAI,aAAa,IAAI,SAAS,IAC9B,IAAI,aAAa,IAAI,eAAe,IACpC,IAAI,aAAa,IAAI,QAAQ;CAC/B,MAAM,aAAa,gBAAgB,MAAM,CAAC,aAAa;AAEvD,KACE,eAAe,WACf,eAAe,iBACf,eAAe,SACf,eAAe,iBAEf,QAAO;AAGT,KACE,eAAe,SACf,eAAe,eACf,eAAe,iBACf,eAAe,aAEf,QAAO;AAGT,KAAI,IAAI,aAAa,IAAI,aAAa,KAAK,UAAU,IAAI,aAAa,IAAI,iBAAiB,KAAK,OAC9F,QAAO;AAGT,QAAO;AACR;AAED,SAAS,uBAAuBP,SAAkBQ,aAA6B;AAC7E,KAAI,gBAAgB,KAAK,YAAY,CACnC,QAAO;AAGT,QAAO,IAAI,IAAI,aAAa,IAAI,IAAI,QAAQ,KAAK,QAAQ,UAAU;AACpE;AAED,eAAe,sBACbC,eACAb,OACAc,eACe;AACf,MAAK,MACH;AAGF,KAAI;AACF,QAAM,KAAK,gBAAgB,eAAe,OAAO,EAC/C,iBAAiB,cAClB,EAAC;CACH,SAAQ,OAAO;AACd,SAAO,KAAK,gCAAgC;GAC1C;GACA,OAAO,qBAAqB,MAAM;EACnC,EAAC;CACH;AACF;AAED,eAAe,8BAA8BC,aAAyC;AACpF,MAAK,YAAY,iBAAiB,YAAY,YAC5C;AAGF,KAAI;EACF,MAAM,gBAAgB,MAAM,gBAAgB;AAC5C,QAAM,sBAAsB,eAAe,YAAY,cAAc,gBAAgB;AACrF,QAAM,sBAAsB,eAAe,YAAY,aAAa,eAAe;CACpF,SAAQ,OAAO;AACd,SAAO,KAAK,gFAAgF,EAC1F,OAAO,qBAAqB,MAAM,CACnC,EAAC;CACH;AACF;AAED,eAAe,0CAA0CC,QAAiC;CACxF,MAAM,WAAW,MAAM,2BAA2B,OAAO;AAEzD,KAAI;EACF,MAAM,gBAAgB,MAAM,gBAAgB;AAC5C,OAAK,MAAM,EAAE,MAAM,IAAI,UAAU;AAC/B,SAAM,sBAAsB,eAAe,KAAK,cAAc,gBAAgB;AAC9E,SAAM,sBAAsB,eAAe,KAAK,aAAa,eAAe;EAC7E;CACF,SAAQ,OAAO;AACd,SAAO,KAAK,uFAAuF,EACjG,OAAO,qBAAqB,MAAM,CACnC,EAAC;CACH;AAED,QAAO,MAAM,2BAA2B,OAAO;AAChD;AAED,SAAS,2BAA2BC,aAAyD;CAC3F,MAAM,SAAS,YAAY,QAAQ;AAEnC,KAAI,OACF,QAAO;AAGT,QAAO,aAAa,YAAY,SAAS,IAAI,CAAE;AAChD;;;;AAKD,eAAe,iBAA8C;CAC3D,MAAM,SAAS,eAAe;CAC9B,MAAM,MAAM,KAAK,KAAK;AAEtB,KAAI,mBAAmB,MAAM,gBAAgB,WAAW,UACtD,QAAO,gBAAgB;AAGzB,KAAI;EACF,MAAM,gBAAgB,MAAM,KAAK,UAC/B,IAAI,IAAI,OAAO,YACf,OAAO,UACP,kBAAkB,OAAO,EACzB,wBAAwB,OAAO,EAC/B,qBAAqB,CACtB;AAED,oBAAkB;GAChB;GACA,UAAU;EACX;AAED,SAAO,KAAK,yCAAyC;GACnD,QAAQ,cAAc,gBAAgB,CAAC;GACvC,UAAU,OAAO;GACjB,kBAAkB,OAAO;EAC1B,EAAC;AACF,SAAO;CACR,SAAQ,OAAO;AACd,SAAO,MAAM,yBAAyB,iBAAiB,QAAQ,gBAAmB;GAChF,WAAW,OAAO;GAClB,UAAU,OAAO;EAClB,EAAC;AACF,QAAM,IAAI,OAAO,yBAAyB,qBAAqB,MAAM,CAAC;CACvE;AACF;AAED,SAAS,QAAQC,SAAwD;CACvE,MAAM,aAAa,UAAU,IAAI,QAAQ;AACzC,KAAI,WACF,QAAO;CAGT,MAAM,OAAO,mBAAmB,IAAI,IAAI,SAAS;AACjD,WAAU,IAAI,SAAS,KAAK;AAC5B,QAAO;AACR;AAED,eAAe,0BAA0Bd,SAA0C;CACjF,MAAM,cAAc,QAAQ,QAAQ,IAAI,eAAe,IAAI;AAE3D,KAAI,YAAY,SAAS,mBAAmB,EAAE;EAC5C,MAAM,OAAO,MAAM,QAAQ,MAAM,CAAC,MAAM,MAAM,KAAK;AACnD,gBAAc,MAAM,iBAAiB,WAAW,KAAK,eAAe;CACrE;CAED,MAAM,WAAW,MAAM,QAAQ,UAAU,CAAC,MAAM,MAAM,KAAK;CAC3D,MAAM,cAAc,UAAU,IAAI,eAAe;AACjD,eAAc,gBAAgB,WAAW,cAAc;AACxD;AAED,SAAS,gCAAgCe,SAAkCC,UAIzE;CACA,MAAM,SAAS,QAAQ;CACvB,MAAM,sBACJ,iBACO,WAAW,aACjB,MAAM,QAAQ,OAAO,IACtB,wDAAwD;AAE1D,MAAK,oBACH,OAAM,IAAI,MAAM;AAGlB,KAAI,WAAW,QACb,OAAM,IAAI,MAAM;CAGlB,MAAM,WAAW,QAAQ;CACzB,MAAM,oBAAoB,MAAM,QAAQ,SAAS,GAC7C,SAAS,SAAS,SAAS,GAC3B,aAAa;AAEjB,MAAK,kBACH,OAAM,IAAI,MAAM;CAGlB,MAAM,iBAAiB,QAAQ,QAAQ,WAAW,QAAQ;CAC1D,MAAM,aAAa,QAAQ,QAAQ,WAAW,QAAQ;AAEtD,MAAK,YAAY,IACf,OAAM,IAAI,MAAM;AAGlB,QAAO;EACL,eAAe,QAAQ,QAAQ,WAAW,QAAQ;EAClD;EACA;CACD;AACF;AAMD,MAAMC,6BAAyD;CAC7D,SAAS,CAAC,KAAM;CAChB,MAAM;EAAC;EAAQ;EAAsB;CAAQ;CAC7C,OAAO,CAAC,OAAQ;CAChB,WAAW,CAAC,cAAc,WAAY;CACtC,YAAY,CAAC,eAAe,YAAa;CACzC,SAAS,CAAC,WAAW,YAAa;CAClC,QAAQ,CAAC,QAAS;CAClB,UAAU,CAAC,UAAW;CACtB,WAAW,CAAC,YAAa;CACzB,WAAW,CAAC,YAAa;CACzB,aAAa,CAAC,cAAe;CAC7B,aAAa;EAAC;EAAa;EAAe;CAAQ;CAClD,aAAa,CAAC,cAAe;AAC9B;AAED,IAAIC,mBAA+C,EACjD,GAAG,2BACJ;AAED,SAAS,mBAAmBC,MAA2B;AACrD,MAAK,MAAM,QAAQ,KAAK,CACtB,QAAO,CAAE;AAGX,QAAO,MAAM,KACX,IAAI,IACF,KACG,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,CACxB,OAAO,CAAC,QAAQ,IAAI,SAAS,EAAE,EAErC;AACF;;;;;AAMD,SAAgB,0BAA0BC,SAAiC;AACzE,oBAAmB;EACjB,SAAS,QAAQ,qBAAwB,mBAAmB,QAAQ,QAAQ,GAAG,2BAA2B;EAC1G,MAAM,QAAQ,kBAAqB,mBAAmB,QAAQ,KAAK,GAAG,2BAA2B;EACjG,OAAO,QAAQ,mBAAsB,mBAAmB,QAAQ,MAAM,GAAG,2BAA2B;EACpG,WAAW,QAAQ,uBAA0B,mBAAmB,QAAQ,UAAU,GAAG,2BAA2B;EAChH,YAAY,QAAQ,wBAA2B,mBAAmB,QAAQ,WAAW,GAAG,2BAA2B;EACnH,SAAS,QAAQ,qBAAwB,mBAAmB,QAAQ,QAAQ,GAAG,2BAA2B;EAC1G,QAAQ,QAAQ,oBAAuB,mBAAmB,QAAQ,OAAO,GAAG,2BAA2B;EACvG,UAAU,QAAQ,sBAAyB,mBAAmB,QAAQ,SAAS,GAAG,2BAA2B;EAC7G,WAAW,QAAQ,uBAA0B,mBAAmB,QAAQ,UAAU,GAAG,2BAA2B;EAChH,WAAW,QAAQ,uBAA0B,mBAAmB,QAAQ,UAAU,GAAG,2BAA2B;EAChH,aAAa,QAAQ,yBAA4B,mBAAmB,QAAQ,YAAY,GAAG,2BAA2B;EACtH,aAAa,QAAQ,yBAA4B,mBAAmB,QAAQ,YAAY,GAAG,2BAA2B;EACtH,aAAa,QAAQ,yBAA4B,mBAAmB,QAAQ,YAAY,GAAG,2BAA2B;CACvH;AACF;;;;AAKD,SAAgB,wBAA8B;AAC5C,oBAAmB,EACjB,GAAG,2BACJ;AACF;AAED,SAAS,gCAA4D;AACnE,QAAO;AACR;AAED,SAAS,mBACPC,QACAC,WACmC;AACnC,MAAK,MAAM,YAAY,UACrB,KAAI,YAAY,OACd,QAAO;EACL,KAAK;EACL,OAAO,OAAO;CACf;AAIL,QAAO,CAAE;AACV;AAED,SAAS,eAAeD,QAAiCC,WAAyC;CAChG,MAAM,EAAE,OAAO,GAAG,mBAAmB,QAAQ,UAAU;AAEvD,YAAW,UAAU,UAAU;EAC7B,MAAM,UAAU,MAAM,MAAM;AAC5B,SAAO,QAAQ,SAAS,IAAI;CAC7B;AAED,YAAW,UAAU,mBAAmB,UAAU,UAChD,SAAQ,EAAE,MAAM;AAGlB;AACD;AAED,SAAS,eAAeD,QAAiCC,WAAyC;CAChG,MAAM,EAAE,OAAO,GAAG,mBAAmB,QAAQ,UAAU;AAEvD,YAAW,UAAU,YAAY,OAAO,SAAS,MAAM,CACrD,QAAO;AAGT,YAAW,UAAU,UAAU;EAC7B,MAAM,SAAS,OAAO,MAAM;AAC5B,SAAO,OAAO,SAAS,OAAO,GAAG;CAClC;AAED;AACD;AAED,SAAS,gBACPD,QACAC,WACA,eAAe,OACN;CACT,MAAM,EAAE,OAAO,GAAG,mBAAmB,QAAQ,UAAU;AAEvD,YAAW,UAAU,UACnB,QAAO;AAGT,YAAW,UAAU,SACnB,QAAO,MAAM,aAAa,KAAK;AAGjC,QAAO;AACR;;;;AAKD,SAAS,UAAUC,OAAwB;AACzC,KAAI;AACF,SAAO,KAAK,MAAM,MAAM;CACzB,QAAO;AACN,SAAO;CACR;AACF;;;;AAKD,SAAS,cAAcC,OAA0B;AAC/C,MAAK,MAAO,QAAO,CAAE;AAErB,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MACJ,IAAI,CAAC,gBAAiB,SAAS,WAAW,KAAK,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,CAAE,CAC1E,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE;AAGtC,YAAW,UAAU,UAAU;EAC7B,MAAM,UAAU,MAAM,MAAM;AAC5B,OAAK,QAAS,QAAO,CAAE;AAEvB,MAAI,QAAQ,WAAW,IAAI,EAAE;GAC3B,MAAM,SAAS,UAAU,QAAQ;AACjC,OAAI,MAAM,QAAQ,OAAO,CACvB,QAAO,OACJ,IAAI,CAAC,gBAAiB,SAAS,WAAW,KAAK,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,CAAE,CAC1E,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE;EAEvC;AAED,MAAI,QAAQ,SAAS,IAAI,CACvB,QAAO,QACJ,MAAM,IAAI,CACV,IAAI,CAAC,SAAS,KAAK,MAAM,CAAC,CAC1B,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE;AAGtC,SAAO,CAAC,OAAQ;CACjB;AAED,QAAO,CAAE;AACV;AAED,SAAS,aAAaA,OAA0B;AAC9C,YAAW,UAAU,SACnB,QAAO,MACJ,MAAM,CACN,MAAM,MAAM,CACZ,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE;AAGtC,QAAO,cAAc,MAAM;AAC5B;;;;AAKD,SAAgB,gBAAgBH,QAM9B;CACA,MAAM,eAAe,+BAA+B;CAGpD,IAAII,UAAoB,CAAE;CAC1B,MAAM,EAAE,OAAO,cAAc,GAAG,mBAAmB,QAAQ,aAAa,UAAU;AAClF,KAAI,wBACF,YAAW,iBAAiB,YAAY,aAAa,MAAM,CAAC,WAAW,IAAI,EAAE;EAC3E,MAAM,SAAS,UAAU,aAAa,MAAM,CAAC;AAC7C,MAAI,MAAM,QAAQ,OAAO,CACvB,WAAU,OACP,IAAI,CAAC,cAAe,OAAO,WAAW,GAAG,MAAM,GAAG,CAAC,EAAE,GAAG,EAAE,MAAM,CAAE,CAClE,OAAO,CAAC,OAAO,GAAG,SAAS,EAAE;CAEnC,MACC,WAAU,cAAc,aAAa;CAKzC,MAAMC,cAAwC,CAAE;CAChD,MAAM,EAAE,OAAO,UAAU,GAAG,mBAAmB,QAAQ,aAAa,YAAY;AAChF,KAAI,qBAAwB;EAC1B,IAAIC,cAAuB;AAE3B,aAAW,aAAa,YAAY,SAAS,MAAM,CAAC,WAAW,IAAI,CACjE,eAAc,UAAU,SAAS,MAAM,CAAC,IAAI;AAG9C,MAAI,sBAAsB,gBAAgB,aAAa,MAAM,QAAQ,YAAY,CAC/E,MAAK,MAAM,CAAC,UAAU,UAAU,IAAI,OAAO,QAAQ,YAAuC,EAAE;GAC1F,MAAM,UAAU,cAAc,UAAU;AACxC,OAAI,QAAQ,SAAS,EACnB,aAAY,YAAY;EAE3B;CAEJ;CAGD,MAAM,kBAAkB,mBAAmB,QAAQ,aAAa,YAAY;CAC5E,MAAM,cACJ,gBAAgB,QAAQ,UACpB,aAAa,gBAAgB,MAAM,GACnC,cAAc,gBAAgB,MAAM;CAG1C,MAAM,cAAc,gBAAgB,QAAQ,aAAa,YAAY;AAErE,QAAO;EAAE;EAAS;EAAa;EAAa;CAAa;AAC1D;;;;AASD,eAAe,kBACbd,aACAQ,QAC+B;CAC/B,MAAM,eAAe,+BAA+B;CACpD,MAAMO,WAAqB;EACzB,KAAK,eAAe,QAAQ,aAAa,QAAQ,IAAI;EACrD,MAAM,eAAe,QAAQ,aAAa,KAAK,IAAI,eAAe,QAAQ,aAAa,MAAM,IAAI;EACjG,OAAO,eAAe,QAAQ,aAAa,MAAM;EACjD,WAAW,eAAe,QAAQ,aAAa,UAAU;EACzD,YAAY,eAAe,QAAQ,aAAa,WAAW;EAC3D,SAAS,eAAe,QAAQ,aAAa,QAAQ;EACrD,QAAQ,eAAe,QAAQ,aAAa,OAAO;EACnD,UAAU,eAAe,QAAQ,aAAa,SAAS;EACvD,YAAY,eAAe,QAAQ,aAAa,UAAU;CAC3D;CAED,MAAM,YAAY,YAAY,aAC1B,KAAK,KAAK,GAAG,YAAY,aAAa;CAE1C,MAAM,MAAM,KAAK,KAAK;CACtB,MAAM,eAAgB,YAAwC;AAE9D,QAAO;EACL,QAAQ,SAAS;EACjB,aAAa,YAAY;EACzB,cAAc,YAAY;EAC1B,SAAS,YAAY;EACrB;EACA,QAAQ,eAAe,QAAQ,CAAC,KAAM,EAAC;EACvC,KAAK,eAAe,QAAQ,CAAC,KAAM,EAAC;EACpC,qBAAqB,iBAAiB,WAAW;EACjD,UAAU,eAAe,CAAC;EAC1B,MAAM;EACN,WAAW;EACX,cAAc;CACf;AACF;AAOD,MAAM,qBAAqB,EAAE,yBAAyB;AACtD,MAAM,0BAA0B,EAAE,yBAAyB;AAC3D,MAAM,sBAAsB,EAAE,yBAAyB;;;;;;;;;;;;;;;;;;;;;AAsBvD,SAAgB,aAAa5B,SAAoC;CAC/D,MAAM,UAAU,QAAQ,QAAQ,IAAI,SAAS;AAC7C,MAAK,QAAS,QAAO;CAGrB,MAAM,aAAa,IAAI,QAAQ,EAAE,kBAAkB;CACnD,MAAM,aAAa,QAAQ,MAAM,WAAW;AAC5C,MAAK,WAAY,QAAO;CAExB,MAAM,QAAQ,mBAAmB,WAAW,GAAG;AAC/C,MAAK,MAAO,QAAO;CAGnB,MAAM,YAAY,IAAI,QAAQ,EAAE,uBAAuB;CACvD,MAAM,YAAY,QAAQ,MAAM,UAAU;CAC1C,MAAM,cAAc,YAAY,mBAAmB,UAAU,GAAG;AAEhE,QAAO;EAAE;EAAO;CAAa;AAC9B;;;;;;AAOD,SAAgB,wBAAiC;CAC/C,MAAM,UAAU,IAAI;AACpB,SAAQ,OAAO,eAAe,EAAE,kBAAkB,sBAAsB;AACxE,SAAQ,OAAO,eAAe,EAAE,uBAAuB,sBAAsB;AAC7E,QAAO;AACR;;;;AAKD,SAAgB,aAAaA,SAA2B;CACtD,MAAM,UAAU,QAAQ,QAAQ,IAAI,SAAS;AAC7C,QAAO,SAAS,UAAU,EAAE,kBAAkB,GAAG,IAAI;AACtD;;;;;;;AAYD,eAAsB,MACpBA,SACA6B,oBACmB;AACnB,KAAI;EACF,MAAM,SAAS,eAAe;EAG9B,MAAMC,iBAA+B,uBAAuB,WACxD,EAAE,WAAW,mBAAoB,IACjC,sBAAsB,CAAE;EAG5B,MAAM,UAAU,QAAQ,QAAQ,IAAI,SAAS;EAC7C,MAAM,WAAW,SAAS,UAAU,EAAE,kBAAkB,GAAG;AAC3D,MAAI,UAAU;AACZ,UAAO,KAAK,uDAAuD;GAEnE,MAAMC,YAAU,IAAI;AACpB,aAAQ,OAAO,eAAe,EAAE,kBAAkB,sBAAsB;GAExE,MAAM,cAAc,IAAI,IAAI,OAAO;GACnC,MAAM,UAAU,YAAY;AAC5B,UAAO,uBAAuB,SAAS,EAAE,mBAAS,EAAC;EACpD;EAED,MAAM,aAAa,MAAM,gBAAgB;EAGzC,MAAM,eAAe,KAAK,wBAAwB;EAClD,MAAM,gBAAgB,MAAM,KAAK,2BAA2B,aAAa;EAEzE,MAAM,kBAAkB,qCAAqC,QAAQ,iBAAiB,QAAQ;EAK9F,MAAM,QAAQ,wBAAwB,KAAK,aAAa,EAAE,QAAQ,mBAAmB;EACrF,MAAM,QAAQ,KAAK,aAAa;EAGhC,MAAMC,aAAyB;GAC7B;GACA;GACA;GACA,WAAW,QAAQ;GACnB,UAAU,QAAQ;GAClB,WAAW,KAAK,KAAK;EACtB;AAED,SAAO,KAAK,wBAAwB;GAClC,WAAW,QAAQ;GACnB,QAAQ,QAAQ;GAChB,UAAU,QAAQ;GAClB,oBAAoB,OAAO,KAAK,gBAAgB;EACjD,EAAC;EAGF,MAAM,UAAU,MAAM,iBAAiB,WAAW;EAElD,MAAMC,0BAAkD;GACtD,cAAc,OAAO;GACrB,OAAO,OAAO;GACd;GACA;GACA,gBAAgB;GAChB,uBAAuB;GACvB,GAAG;EACJ;AAED,MAAI,QAAQ,OACV,yBAAwB,SAAS,QAAQ;AAG3C,MAAI,QAAQ,SACV,yBAAwB,YAAY,QAAQ;EAG9C,MAAM,mBAAmB,KAAK,sBAAsB,YAAY,wBAAwB;EAGxF,MAAM,UAAU,IAAI;AACpB,UAAQ,OAAO,eAAe,EAAE,mBAAmB,GAAG,QAAQ,+CAA+C;AAE7G,SAAO,KAAK,qCAAqC;GAC/C,UAAU,OAAO;GACjB,aAAa,OAAO;GACpB,OAAO,OAAO;GACd,uBAAuB,WAAW,gBAAgB,CAAC;GACnD;GACA,QAAQ,QAAQ;GAChB,UAAU,QAAQ;EACnB,EAAC;AAEF,SAAO,uBAAuB,iBAAiB,UAAU,EAAE,EAAE,QAAS,EAAC;CACxE,SAAQ,OAAO;AACd,SAAO,MAAM,kCAAkC,iBAAiB,QAAQ,eAAkB;AAC1F,QAAM,IAAI,MAAM;CACjB;AACF;;;;;;;;;;;;;AAcD,eAAsB,eAAejC,SAAqC;AACxE,KAAI;EACF,MAAM,SAAS,eAAe;EAC9B,MAAM,iBAAiB,2BAA2B,OAAO;EACzD,MAAM,MAAM,IAAI,IAAI,QAAQ;EAC5B,MAAM,OAAO,IAAI,aAAa,IAAI,OAAO;EACzC,MAAM,QAAQ,IAAI,aAAa,IAAI,QAAQ;EAC3C,MAAM,QAAQ,IAAI,aAAa,IAAI,QAAQ;EAC3C,MAAM,mBAAmB,IAAI,aAAa,IAAI,oBAAoB;EAClE,MAAM,WAAW,IAAI,aAAa,IAAI,YAAY;EAClD,MAAM,iBAAiB,IAAI,aAAa,IAAI,mBAAmB;AAE/D,SAAO,KAAK,0BAA0B;GACpC,WAAW;GACX,YAAY;GACZ;GACA;GACA;GACA;GACA,UAAU,OAAO;GACjB,aAAa,OAAO;EACrB,EAAC;AAGF,MAAI,OAAO;AACT,OAAI,kBAAkB,OAAO;IAC3B,MAAMkC,YAAU,QAAQ,QAAQ,IAAI,SAAS;IAC7C,MAAMC,oBAAkB,IAAI,QAAQ,EAAE,mBAAmB;IACzD,MAAMC,gBAAc,WAAS,MAAMD,kBAAgB;IACnD,MAAME,YAAUD,gBAAc;AAE9B,QAAIC,WAAS;KACX,MAAMC,eAAa,MAAM,cAAcD,UAAQ;KAC/C,MAAME,aAAWD,eAAa,KAAK,KAAK,GAAGA,aAAW,YAAY,OAAO;AAEzE,SAAIA,gBAAcA,aAAW,UAAU,SAASC,cAAY,KAAK,KAAK,KAAM;AAC1E,YAAM,iBAAiBF,UAAQ;MAC/B,MAAMN,YAAU,IAAI;AACpB,gBAAQ,OAAO,eAAe,EAAE,mBAAmB,8CAA8C;MAEjG,MAAMS,gBAAc,uCAClBF,aAAW,aAAa,KACxB,YAAYA,aAAW,UACvB,eACD;AAED,aAAO,KAAK,0DAA0D;OACpE;OACA;OACA,UAAU,YAAYA,aAAW;OACjC;MACD,EAAC;AAEF,aAAO,uBAAuBE,eAAa,EAAE,mBAAS,EAAC;KACxD;IACF;GACF;AAED,UAAO,MAAM,gCAAmC;IAC9C;IACA;IACA,UAAU,OAAO;IACjB,iBAAiB,OAAO;IACxB;GACD,EAAC;AAIF,OAAI,UAAU,iBAAiB;AAC7B,WAAO,KAAK,sCAAsC,EAAE,UAAU,OAAO,SAAU,EAAC;IAEhF,MAAMT,YAAU,MAAM,mBAAmB,QAAQ;AAIjD,cAAQ,OACN,eACC,EAAE,kBAAkB,6DACtB;AACD,cAAQ,OACN,eACC,EAAE,uBAAuB,GAAG,mBAAmB,oBAAoB,8CAA8C,CAAC,qCACpH;IAGD,MAAMU,gBAAc,IAAI,IAAI,OAAO;IACnC,MAAMC,YAAUD,cAAY;AAC5B,WAAO,KAAK,0DAA0D;KACpE;KACA;KACA,mBAAmB,OAAO;IAC3B,EAAC;AAGF,WAAO,wBAAwB,EAAEC,UAAQ,cAAc,EAAE,mBAAS,EAAC;GACpE;AAGD,OAAI,UAAU,kBAAkB;AAC9B,WAAO,KAAK,gDAAgD;IAC5D,MAAMX,YAAU,MAAM,mBAAmB,QAAQ;IAEjD,MAAMU,gBAAc,IAAI,IAAI,OAAO;IACnC,MAAMC,YAAUD,cAAY;AAC5B,WAAO,uBAAuBC,WAAS,EAAE,mBAAS,EAAC;GACpD;GAGD,MAAMX,YAAU,MAAM,mBAAmB,QAAQ;AACjD,aAAQ,OACN,eACC,EAAE,kBAAkB,GAAG,mBAAmB,MAAM,CAAC,+CACnD;AACD,OAAI,iBACF,WAAQ,OACN,eACC,EAAE,uBAAuB,GAAG,mBAAmB,iBAAiB,CAAC,qCACnE;GAGH,MAAM,cAAc,IAAI,IAAI,OAAO;GACnC,MAAM,UAAU,YAAY;AAC5B,UAAO,uBAAuB,SAAS,EAAE,mBAAS,EAAC;EACpD;EAGD,MAAM,UAAU,QAAQ,QAAQ,IAAI,SAAS;EAC7C,MAAM,kBAAkB,IAAI,QAAQ,EAAE,mBAAmB;EACzD,MAAM,cAAc,SAAS,MAAM,gBAAgB;EACnD,MAAM,UAAU,cAAc;AAE9B,OAAK,QACH,OAAM,IAAI,MAAM;EAIlB,MAAM,aAAa,MAAM,cAAc,QAAQ;AAE/C,OAAK,WACH,OAAM,IAAI,MAAM;AAGlB,OAAK,MACH,OAAM,IAAI,MAAM;AAGlB,MAAI,WAAW,UAAU,MACvB,OAAM,IAAI,OAAO;EAInB,MAAM,WAAW,KAAK,KAAK,GAAG,WAAW;AACzC,MAAI,WAAW,KAAK,KAAK,IACvB,OAAM,IAAI,MAAM;AAGlB,OAAK,MAAM;AACT,OAAI,gBAAgB;AAClB,UAAM,iBAAiB,QAAQ;IAC/B,MAAMA,YAAU,IAAI;AACpB,cAAQ,OAAO,eAAe,EAAE,mBAAmB,8CAA8C;IAEjG,MAAMS,gBAAc,uCAClB,WAAW,aAAa,KACxB,YAAY,WAAW,UACvB,eACD;AAED,WAAO,KAAK,oEAAoE;KAC9E,UAAU,YAAY,WAAW;KACjC;IACD,EAAC;AAEF,WAAO,uBAAuBA,eAAa,EAAE,mBAAS,EAAC;GACxD;AAED,SAAM,IAAI,MAAM;EACjB;AAED,SAAO,KAAK,4BAA4B;EAExC,MAAM,aAAa,MAAM,gBAAgB;EACzC,MAAM,cAAc,MAAM,KAAK,uBAAuB,YAAY,KAAK;GACrE,kBAAkB,WAAW;GAC7B,eAAe,WAAW;GAC1B,eAAe,WAAW;GAC1B,iBAAiB,OAAO,MAAM,MAAM,MAAM,CAAC,SAAS,SAAS;EAC9D,EAAC;AAEF,SAAO,KAAK,wCAAwC;EAEpD,MAAM,SAAS,2BAA2B,YAAY;EAGtD,MAAM,iBAAiB,MAAM,kBAAkB,aAAa,OAAO;EAGnE,MAAM,iBAAiB,MAAM,kBAAkB,SAAS,eAAe;AAGvE,QAAM,iBAAiB,QAAQ;EAG/B,MAAM,UAAU,IAAI,QAAQ;AAC5B,UAAQ,OAAO,eAAe,EAAE,mBAAmB,8CAA8C;AAEjG,SAAO,KAAK,wCAAwC;EAGpD,MAAM,cAAc,uCAClB,WAAW,aAAa,KACxB,YAAY,WAAW,UACvB,eACD;AACD,SAAO,uBAAuB,aAAa,EAAE,QAAS,EAAC;CACxD,SAAQ,OAAO;AACd,SAAO,MAAM,yBAAyB,iBAAiB,QAAQ,eAAkB;AAGjF,MAAI;GACF,MAAM,UAAU,QAAQ,QAAQ,IAAI,SAAS;GAC7C,MAAM,kBAAkB,IAAI,QAAQ,EAAE,mBAAmB;GACzD,MAAM,cAAc,SAAS,MAAM,gBAAgB;GACnD,MAAM,UAAU,cAAc;AAC9B,OAAI,QACF,OAAM,iBAAiB,QAAQ;EAElC,QAAO,CAEP;AAED,QAAM,mBAAmB,QAAQ;AACjC,QAAM,IAAI,MAAM;CACjB;AACF;;;;;;;;;AAUD,eAAsB,OAAOxC,SAAqC;AAChE,KAAI;EACF,MAAM,SAAS,eAAe;EAC9B,MAAM,MAAM,IAAI,IAAI,QAAQ;EAE5B,MAAM,cAAc,eAAe,IAAI;EACvC,MAAM,YAAY,wBAAwB,IAAI,aAAa,IAAI,YAAY,EAAE,QAAQ;AAErF,SAAO,KAAK,oBAAoB;GAC9B,UAAU,OAAO;GACjB;EACD,EAAC;EAEF,MAAM,cAAc,MAAM,eAAe,QAAQ;EACjD,MAAM,UAAU,YAAY;AAE5B,MAAI,gBAAgB,SAAS;GAI3B,MAAM+B,YAAU,MAAM,mBAAmB,QAAQ;GACjD,MAAM,cAAc,aAAa;GACjC,MAAM,sBAAsB,uBAAuB,SAAS,YAAY;AAExE,aAAQ,OAAO,cAAc,gEAAgE;AAC7F,UAAO,KAAK,sCAAsC,EAAE,aAAa,oBAAqB,EAAC;AACvF,UAAO,uBAAuB,qBAAqB,EAAE,mBAAS,EAAC;EAChE;EAED,MAAM,iBAAiB,IAAI;AAE3B,MAAI,gBAAgB,SAAS,YAAY,QAAQ;AAC/C,SAAM,0CAA0C,YAAY,OAAO;AACnE,kBAAe,OAAO,cAAc,gEAAgE;GACpG,MAAM,iBAAiB,MAAM,mBAAmB,QAAQ;AACxD,kBAAe,QAAQ,CAAC,OAAO,QAAQ,eAAe,OAAO,KAAK,MAAM,CAAC;EAC1E,OAAM;AACL,SAAM,8BAA8B,YAAY;GAChD,MAAM,iBAAiB,MAAM,mBAAmB,QAAQ;AACxD,kBAAe,QAAQ,CAAC,OAAO,QAAQ,eAAe,OAAO,KAAK,MAAM,CAAC;EAC1E;EAED,MAAM,UAAU,IAAI,QAAQ;AAG5B,MAAI,UACF,SAAQ,OACN,eACC,oBAAoB,mBAAmB,UAAU,CAAC,+CACpD;MAED,SAAQ,OAAO,cAAc,gEAAgE;AAG/F,MAAI;GACF,MAAM,aAAa,MAAM,gBAAgB;GACzC,MAAM,iBAAiB,WAAW,gBAAgB;GAClD,MAAMY,mBAA2C,CAAE;GACnD,MAAM,iBAAiB,eAAe,UAAU,OAAO;AAEvD,OAAI,WAAW,kBAAkB,SAAS,eAAe,CACvD,kBAAiB,gBAAgB;YACxB,QACT,QAAO,KAAK,mGAAmG;IAC7G,aAAa,aAAa,QAAQ;IAClC,gBAAgB,gBAAgB,eAAe;IAC/C,UAAU,OAAO;GAClB,EAAC;AAGJ,OAAI,OAAO,sBACT,kBAAiB,2BAA2B,OAAO;GAGrD,MAAM,gBAAgB,KAAK,mBAAmB,YAAY,iBAAiB;AAC3E,UAAO,KAAK,wCAAwC;IAClD,oBAAoB,eAAe;IACnC,gBAAgB,QAAQ,iBAAiB,cAAc;GACxD,EAAC;AAEF,UAAO,uBAAuB,cAAc,UAAU,EAAE,EAAE,QAAS,EAAC;EACrE,SAAQ,OAAO;AACd,UAAO,KAAK,yEAAyE,EACnF,OAAO,qBAAqB,MAAM,CACnC,EAAC;EACH;EAGD,MAAM,iBAAiB,OAAO,yBAAyB;EACvD,MAAM,mBAAmB,uBAAuB,SAAS,eAAe;AAExE,SAAO,KAAK,oBAAoB,EAAE,aAAa,iBAAkB,EAAC;AAClE,SAAO,uBAAuB,kBAAkB,EAAE,QAAS,EAAC;CAC7D,SAAQ,OAAO;AACd,SAAO,MAAM,iBAAiB,iBAAiB,QAAQ,eAAkB;EACzE,MAAM,UAAU,MAAM,mBAAmB,QAAQ;AACjD,SAAO,uBAAuB,KAAK,EAAE,QAAS,EAAC;CAChD;AACF;;;;;;;;;AAUD,eAAsB,wBAAwB3C,SAAqC;AACjF,KAAI,QAAQ,WAAW,OACrB,QAAO,SAAS,KAAK,EAAE,OAAO,qBAAsB,GAAE,EAAE,QAAQ,IAAK,EAAC;AAGxE,KAAI;EACF,MAAM,SAAS,eAAe;EAC9B,MAAM,aAAa,MAAM,gBAAgB;EACzC,MAAM,iBAAiB,WAAW,gBAAgB;EAClD,MAAM,cAAc,MAAM,0BAA0B,QAAQ;AAE5D,OAAK,YACH,QAAO,SAAS,KAAK,EAAE,OAAO,uBAAwB,GAAE,EAAE,QAAQ,IAAK,EAAC;AAG1E,OAAK,eAAe,SAClB,OAAM,IAAI,MAAM;EAGlB,MAAM,EAAE,SAAS,GAAG,MAAM,UAAU,aAAa,QAAQ,eAAe,SAAS,EAAE;GACjF,QAAQ,eAAe,UAAU,OAAO;GACxC,UAAU,OAAO;GACjB,gBAAgB;EACjB,EAAC;EAEF,MAAM,SAAS,gCAAgC,SAAoC,OAAO,SAAS;EACnG,MAAM,oBAAoB,MAAM,qCAAqC;GACnE,KAAK,OAAO;GACZ,QAAQ,OAAO;EAChB,EAAC;EACF,MAAM4C,SAAkC;GACtC;GACA,QAAQ,OAAO;GACf,SAAS,OAAO;GAChB,KAAK,OAAO;EACb;AAED,SAAO,KAAK,iCAAiC,EAAE,GAAG,OAAQ,EAAC;AAC3D,SAAO,SAAS,KAAK;GAAE,SAAS;GAAM,GAAG;EAAQ,EAAC;CACnD,SAAQ,OAAO;AACd,SAAO,MAAM,8BAA8B,iBAAiB,QAAQ,eAAkB;AACtF,SAAO,SAAS,KACd;GAAE,OAAO;GAAwB,mBAAmB,qBAAqB,MAAM;EAAE,GACjF,EAAE,QAAQ,IAAK,EAChB;CACF;AACF;;;;;;;AAQD,eAAsB,yBAAyB5C,SAAqC;AAClF,KAAI;EACF,MAAM,SAAS,eAAe;EAC9B,MAAM,aAAa,MAAM,gBAAgB;EACzC,MAAM,iBAAiB,WAAW,gBAAgB;EAClD,MAAM,MAAM,IAAI,IAAI,QAAQ;EAC5B,MAAM,SAAS,IAAI,aAAa,IAAI,MAAM;EAC1C,MAAM,MAAM,IAAI,aAAa,IAAI,MAAM;EACvC,MAAM,iBAAiB,gBAAgB,eAAe,UAAU,OAAO,UAAU;AAEjF,OAAK,UAAU,gBAAgB,OAAO,KAAK,eACzC,QAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAK;EAG3C,MAAM,oBAAoB,MAAM,MAAM,yBAAyB,IAAI,GAAG;EACtE,MAAM6C,SAAmC;GACvC;GACA;GACA,KAAK;EACN;AAED,SAAO,KAAK,kCAAkC,EAAE,GAAG,OAAQ,EAAC;AAC5D,SAAO,IAAI,SAAS,MAAM;GACxB,QAAQ;GACR,SAAS,EACP,iBAAiB,WAClB;EACF;CACF,SAAQ,OAAO;AACd,SAAO,KAAK,gCAAgC,EAC1C,OAAO,qBAAqB,MAAM,CACnC,EAAC;AAEF,SAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAK;CAC1C;AACF;;;;AAKD,eAAsB,QAAQ7C,SAA4C;AACxE,KAAI;EACF,MAAM,cAAc,MAAM,eAAe,QAAQ;AACjD,SAAO,YAAY,QAAQ;CAC5B,SAAQ,OAAO;AACd,SAAO,MAAM,sBAAsB,iBAAiB,QAAQ,eAAkB;AAC9E,SAAO;CACR;AACF;;;;AAKD,eAAsB,YAAYA,SAAqC;CACrE,MAAM,OAAO,MAAM,QAAQ,QAAQ;AAEnC,MAAK,MAAM;EACT,MAAM,MAAM,IAAI,IAAI,QAAQ;EAC5B,MAAM,aAAa,EAAE,IAAI,SAAS,EAAE,IAAI,OAAO;AAC/C,SAAO,KAAK,sCAAsC,EAAE,UAAW,EAAC;AAChE,QAAM,MAAM,MAAM,SAAS,UAAU;CACtC;AAED,QAAO;AACR;;;;AAKD,eAAsB,eAAeA,SAA0C;AAC7E,KAAI;EACF,MAAM,cAAc,MAAM,eAAe,QAAQ;AACjD,SAAO,YAAY,eAAe;CACnC,SAAQ,OAAO;AACd,SAAO,MAAM,8BAA8B,iBAAiB,QAAQ,eAAkB;AACtF,SAAO;CACR;AACF;;;;AAKD,eAAsB,cACpBA,SACA8C,cAC6B;AAC7B,KAAI;EACF,MAAM,cAAc,MAAM,eAAe,QAAQ;EACjD,MAAM,iBAAiB,gBAAgB,YAAY;AAEnD,OAAK,gBAAgB;AACnB,UAAO,KAAK,6BAA6B;AACzC,UAAO;IAAE,SAAS;IAAO,OAAO;IAA8B,cAAc;GAAM;EACnF;AAED,SAAO,KAAK,2BAA2B;EAEvC,MAAM,aAAa,MAAM,gBAAgB;EACzC,MAAM,gBAAgB,MAAM,KAAK,kBAAkB,YAAY,eAAe;AAE9E,SAAO,KAAK,2BAA2B;EAEvC,MAAM,kBAAkB,cAAc,iBAAiB;EACvD,MAAM,YAAY,cAAc,aAC5B,KAAK,KAAK,GAAG,cAAc,aAAa,MACxC,YAAY;AAGhB,QAAM,kBAAkB,SAAS;GAC/B,aAAa,cAAc;GAC3B,cAAc;GACd,SAAS,cAAc;GACvB;GACA,cAAc,KAAK,KAAK;EACzB,EAAC;AAEF,SAAO;GACL,SAAS;GACT,QAAQ;IACN,cAAc,cAAc;IAC5B,eAAe;IACf,UAAU,cAAc;IACxB,YAAY,cAAc;GAC3B;EACF;CACF,SAAQ,OAAO;AACd,SAAO,MAAM,uCAAuC,iBAAiB,QAAQ,eAAkB;AAE/F,SAAO;GACL,SAAS;GACT,OAAO,qBAAqB,MAAM;GAClC,cAAc,4BAA4B,MAAM;EACjD;CACF;AACF;;;;AAQD,SAAgB,uBAA6B;AAC3C,mBAAkB;AAClB,WAAU,OAAO;AAClB"}