{"version":3,"file":"react.mjs","names":[],"sources":["../src/react/context.tsx","../src/react/hooks/useGrant.ts","../src/react/components/GrantGate.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, type ReactNode, useContext, useMemo } from 'react';\n\nimport { GrantClient } from '../grant-client';\nimport type { GrantClientConfig } from '../types';\n\n/**\n * Context for the Grant client\n */\nconst GrantContext = createContext<GrantClient | null>(null);\n\n/**\n * Props for the GrantProvider component\n */\nexport interface GrantProviderProps {\n  /**\n   * Grant client configuration\n   */\n  config: GrantClientConfig;\n\n  /**\n   * Pre-configured GrantClient instance (alternative to config)\n   * If provided, config is ignored\n   */\n  client?: GrantClient;\n\n  /**\n   * Child components\n   */\n  children: ReactNode;\n}\n\n/**\n * Provider component that makes the Grant client available to child components\n *\n * @example\n * ```tsx\n * // Option 1: Pass config (cookie-based refresh)\n * <GrantProvider\n *   config={{\n *     apiUrl: 'https://api.grant.com',\n *     getAccessToken: () => localStorage.getItem('accessToken'),\n *     onRefreshWithCredentials: async () => {\n *       const res = await fetch('https://api.grant.com/api/auth/refresh', { method: 'POST', credentials: 'include' });\n *       if (!res.ok) return false;\n *       const { data } = await res.json();\n *       if (data?.accessToken) { localStorage.setItem('accessToken', data.accessToken); return true; }\n *       return false;\n *     },\n *     onTokenRefresh: (tokens) => { localStorage.setItem('accessToken', tokens.accessToken); },\n *     onUnauthorized: () => { window.location.href = '/login'; },\n *   }}\n * >\n *   <App />\n * </GrantProvider>\n *\n * // Option 2: Pass pre-configured client\n * const grant = new GrantClient({ ... });\n * <GrantProvider client={grant}>\n *   <App />\n * </GrantProvider>\n * ```\n */\nexport function GrantProvider({ config, client, children }: GrantProviderProps) {\n  const grantClient = useMemo(() => {\n    if (client) return client;\n    return new GrantClient(config);\n  }, [client, config]);\n\n  return <GrantContext.Provider value={grantClient}>{children}</GrantContext.Provider>;\n}\n\n/**\n * Hook to access the Grant client from context\n *\n * @throws Error if used outside of GrantProvider\n *\n * @example\n * ```tsx\n * const grant = useGrantClient();\n * const hasPermission = await grant.can('resource', 'action');\n * ```\n */\nexport function useGrantClient(): GrantClient {\n  const client = useContext(GrantContext);\n\n  if (!client) {\n    throw new Error(\n      'useGrantClient must be used within a GrantProvider. ' +\n        'Wrap your app with <GrantProvider config={...}> to fix this error.'\n    );\n  }\n\n  return client;\n}\n\n/**\n * Hook to optionally access the Grant client\n * Returns null if not in a GrantProvider context\n *\n * Use this when you want to gracefully handle missing provider\n */\nexport function useGrantClientOptional(): GrantClient | null {\n  return useContext(GrantContext);\n}\n","'use client';\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport type { AuthorizationResult, Scope } from '../../types';\nimport { useGrantClient } from '../context';\n\n/**\n * Options for the useGrant hook\n */\nexport interface UseGrantOptions {\n  /** Scope to check the permission in. If provided but null/undefined, hook waits for it to become valid. */\n  scope?: Scope | null;\n  /** Whether to skip the permission check */\n  enabled?: boolean;\n  /** Whether to use cached results (default: true) */\n  useCache?: boolean;\n  /** Whether to return loading state (default: false) */\n  returnLoading?: boolean;\n  /** Context to check permissions for */\n  context?: {\n    resource?: Record<string, unknown> | null;\n  };\n}\n\n/**\n * Result when returnLoading is true\n */\nexport interface UseGrantResult {\n  /** Whether the user is granted permission */\n  isGranted: boolean;\n  /** Whether the permission check is loading */\n  isLoading: boolean;\n}\n\n/**\n * Serialize scope for stable dependency comparison\n * This prevents re-fetching when scope object reference changes but values are the same\n */\nfunction serializeScope(scope?: Scope | null): string {\n  if (!scope) return '';\n  return `${scope.tenant}:${scope.id}`;\n}\n\n/**\n * Hook to check if a user is granted permission for a specific resource and action\n *\n * By default, returns a simple boolean, defaulting to false while loading.\n * Set `returnLoading: true` to get an object with `isGranted` and `isLoading`.\n *\n * @param resource - The resource slug to check\n * @param action - The action to check\n * @param options - Additional options\n *\n * @example\n * ```tsx\n * // Simple boolean (default)\n * const canEdit = useGrant('document', 'update');\n *\n * return (\n *   <div>\n *     {canEdit && <EditButton />}\n *   </div>\n * );\n *\n * // With loading state\n * const { isGranted, isLoading } = useGrant('document', 'update', {\n *   returnLoading: true,\n * });\n *\n * if (isLoading) return <Spinner />;\n * if (!isGranted) return null;\n *\n * return <EditButton />;\n * ```\n */\nexport function useGrant(\n  resource: string,\n  action: string,\n  options: UseGrantOptions = {}\n): boolean | UseGrantResult {\n  const { scope, enabled = true, useCache = true, returnLoading = false, context } = options;\n  const client = useGrantClient();\n\n  // Track if scope was explicitly provided (even if null/undefined)\n  // This allows us to distinguish between \"scope not provided\" (optional) vs \"scope provided but falsy\" (wait for it)\n  // Check this once at the start - if scope key exists in options, it was provided\n  // Note: { scope: undefined } has the key, { } does not have the key\n  const scopeWasProvidedRef = useRef('scope' in options);\n\n  // Determine if we should wait for scope to become valid\n  // If scope was provided but is falsy or invalid, wait for it to become truthy\n  // Recalculate when scope changes\n  const isEffectivelyEnabled = useMemo(() => {\n    const hasValidScope =\n      scope && typeof scope === 'object' && 'tenant' in scope && 'id' in scope && scope.id;\n    const shouldWaitForScope = scopeWasProvidedRef.current && !hasValidScope;\n    return enabled && !shouldWaitForScope;\n  }, [scope, enabled]);\n\n  const [data, setData] = useState<AuthorizationResult | null>(null);\n  const [isLoading, setIsLoading] = useState(isEffectivelyEnabled);\n\n  // Synchronously correct isLoading when isEffectivelyEnabled transitions.\n  // useState only uses its initializer on first render, so subsequent transitions\n  // leave isLoading stale for one render cycle (the effect hasn't run yet).\n  // This uses React's \"storing information from previous renders\" pattern to\n  // immediately set isLoading before the render completes.\n  // See: https://react.dev/reference/react/useState#storing-information-from-previous-renders\n  const [prevEffectivelyEnabled, setPrevEffectivelyEnabled] = useState(isEffectivelyEnabled);\n  if (isEffectivelyEnabled !== prevEffectivelyEnabled) {\n    setPrevEffectivelyEnabled(isEffectivelyEnabled);\n    if (isEffectivelyEnabled) {\n      setIsLoading(true);\n    } else {\n      setIsLoading(false);\n      setData(null);\n    }\n  }\n\n  // Track mounted state to prevent state updates after unmount\n  const isMounted = useRef(true);\n\n  // Store scope in a ref so we always have the latest value without triggering re-renders\n  const scopeRef = useRef(scope);\n  scopeRef.current = scope;\n\n  // Store context in a ref so the callback always sends the latest context\n  const contextRef = useRef(context);\n  contextRef.current = context;\n\n  // Serialize scope to get a stable string for dependency comparison\n  const scopeKey = serializeScope(scope);\n\n  // Serialize context so we re-create the callback when context meaningfully changes\n  const contextKey = useMemo(\n    () => (context?.resource != null ? JSON.stringify(context.resource) : ''),\n    [context?.resource]\n  );\n\n  const fetchPermission = useCallback(async () => {\n    if (!isEffectivelyEnabled) {\n      setIsLoading(false);\n      return;\n    }\n\n    setIsLoading(true);\n\n    try {\n      // Use scopeRef.current and contextRef.current to get the latest values\n      // Convert null to undefined for the client (which expects Scope | undefined)\n      const result = await client.isAuthorized(resource, action, {\n        scope: scopeRef.current ?? undefined,\n        useCache,\n        context: contextRef.current,\n      });\n      if (isMounted.current) {\n        setData(result);\n      }\n    } catch {\n      // On error, set data to null (will return false)\n      if (isMounted.current) {\n        setData(null);\n      }\n    } finally {\n      if (isMounted.current) {\n        setIsLoading(false);\n      }\n    }\n    // contextKey ensures we re-run when context (e.g. resource) changes so the request gets the latest context\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [client, resource, action, scopeKey, isEffectivelyEnabled, useCache, contextKey]);\n\n  useEffect(() => {\n    isMounted.current = true;\n\n    // Clear data when scope becomes invalid (waiting for valid scope)\n    if (!isEffectivelyEnabled && scopeWasProvidedRef.current) {\n      setData(null);\n      setIsLoading(false);\n    } else {\n      fetchPermission();\n    }\n\n    return () => {\n      isMounted.current = false;\n    };\n  }, [fetchPermission, isEffectivelyEnabled]);\n\n  const isGranted = data?.authorized ?? false;\n\n  // Return object with loading state if requested, otherwise just boolean\n  if (returnLoading) {\n    return { isGranted, isLoading };\n  }\n\n  return isGranted;\n}\n","'use client';\n\nimport type { ReactNode } from 'react';\n\nimport { useGrant, type UseGrantOptions } from '../hooks/useGrant';\n\n/**\n * Props for the GrantGate component\n */\nexport interface GrantGateProps extends UseGrantOptions {\n  /** The resource slug to check permission for */\n  resource: string;\n  /** The action to check */\n  action: string;\n  /** Content to render if permission is granted */\n  children: ReactNode;\n  /** Content to render if permission is denied (optional) */\n  fallback?: ReactNode;\n  /** Content to render while loading (optional) */\n  loading?: ReactNode;\n}\n\n/**\n * Component that conditionally renders children based on permissions\n *\n * @example\n * ```tsx\n * // Basic usage - hide element if no permission\n * <GrantGate resource=\"document\" action=\"update\">\n *   <EditButton />\n * </GrantGate>\n *\n * // With fallback for denied access\n * <GrantGate\n *   resource=\"admin\"\n *   action=\"access\"\n *   fallback={<p>You don't have admin access</p>}\n * >\n *   <AdminPanel />\n * </GrantGate>\n *\n * // With loading state\n * <GrantGate\n *   resource=\"report\"\n *   action=\"view\"\n *   loading={<Spinner />}\n *   fallback={<AccessDenied />}\n * >\n *   <ReportViewer />\n * </GrantGate>\n *\n * // With scope for multi-tenant\n * <GrantGate\n *   resource=\"project\"\n *   action=\"delete\"\n *   scope={{ tenant: 'project', id: projectId }}\n * >\n *   <DeleteProjectButton />\n * </GrantGate>\n * ```\n */\nexport function GrantGate({\n  resource,\n  action,\n  scope,\n  enabled,\n  useCache,\n  children,\n  fallback = null,\n  loading = null,\n}: GrantGateProps): ReactNode {\n  // Build options object conditionally\n  // Only include scope in options if it's not undefined (null is valid and means \"wait for it\")\n  // This allows the hook to distinguish between \"scope not provided\" (undefined) vs \"scope provided but null\"\n  const options: Parameters<typeof useGrant>[2] = {\n    enabled,\n    useCache,\n    returnLoading: loading !== null,\n  };\n\n  // Only add scope to options if it's explicitly null or a valid object\n  // If scope is undefined, don't include it so hook treats it as optional\n  if (scope !== undefined) {\n    options.scope = scope;\n  }\n\n  // Use loading state if loading prop is provided\n  const result = useGrant(resource, action, options);\n\n  const isGranted = typeof result === 'boolean' ? result : result.isGranted;\n  const isLoading = typeof result === 'boolean' ? false : result.isLoading;\n\n  if (isLoading && loading !== null) {\n    return loading;\n  }\n\n  if (isGranted) {\n    return children;\n  }\n\n  return fallback;\n}\n"],"mappings":"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"}