{"version":3,"file":"grant-client-B-FvCDOz.cjs","names":[],"sources":["../src/grant-client.ts"],"sourcesContent":["import type {\n  AuthorizationResult,\n  GrantClientConfig,\n  PermissionQueryOptions,\n  Scope,\n  SignInWithProjectAppOptions,\n} from './types';\n\n/**\n * Module-level shared promise for cookie-only credential refresh so that all 401s\n * (across all GrantClient instances and in-flight requests) coalesce into one refresh.\n */\nlet sharedCredentialsRefreshPromise: Promise<boolean> | null = null;\n\n/**\n * Module-level shared promise for MFA step-up so that concurrent 403 MFA_REQUIRED\n * responses coalesce into a single step-up dialog.\n */\nlet sharedMfaStepUpPromise: Promise<boolean> | null = null;\n\n/**\n * Grant Client for browser applications\n *\n * Makes HTTP requests to the Grant API to check permissions\n * and retrieve authorization data. Supports both token-based\n * and cookie-based authentication with automatic token refresh.\n */\nexport class GrantClient {\n  private config: Required<Pick<GrantClientConfig, 'apiUrl'>> & GrantClientConfig;\n  private cache: Map<string, { data: unknown; expires: number }> = new Map();\n  private defaultTtl: number;\n\n  constructor(config: GrantClientConfig) {\n    this.config = config;\n    this.defaultTtl = config.cache?.ttl ?? 5 * 60 * 1000; // 5 minutes default\n  }\n\n  // ============================================================================\n  // Public API - Permission Checks\n  // ============================================================================\n\n  /**\n   * Check if the current user has a specific permission\n   *\n   * @example\n   * ```ts\n   * const canEdit = await grant.can('document', 'update');\n   * if (canEdit) {\n   *   // Show edit button\n   * }\n   * ```\n   */\n  async can(resource: string, action: string, options?: PermissionQueryOptions): Promise<boolean> {\n    const result = await this.isAuthorized(resource, action, options);\n    return result.authorized;\n  }\n\n  /**\n   * Alias for `can` - check if user has permission\n   */\n  async hasPermission(\n    resource: string,\n    action: string,\n    options?: PermissionQueryOptions\n  ): Promise<boolean> {\n    return this.can(resource, action, options);\n  }\n\n  // ============================================================================\n  // Public API - Project OAuth (sign-in with project app)\n  // ============================================================================\n\n  /**\n   * Start project-app OAuth flow (redirect only).\n   * Navigates the current window to the Grant OAuth entry page; after sign-in and consent,\n   * the user is redirected to the app's `redirect_uri` with token in the URL fragment.\n   *\n   * Requires `config.frontendUrl` and `redirectUri`.\n   */\n  async signInWithProjectApp(options: SignInWithProjectAppOptions): Promise<void> {\n    const frontendUrl = this.config.frontendUrl;\n    if (!frontendUrl) {\n      throw new Error('GrantClient: frontendUrl is required for signInWithProjectApp');\n    }\n    const locale = options.locale ?? 'en';\n    const redirectUri = options.redirectUri;\n    if (!redirectUri) {\n      throw new Error('redirectUri is required for signInWithProjectApp');\n    }\n\n    const entryPath = `/${locale}/auth/project`;\n    const params = new URLSearchParams({\n      client_id: options.clientId,\n      redirect_uri: redirectUri,\n      state: options.state ?? '',\n    });\n    if (options.scope) params.set('scope', options.scope);\n\n    const entryUrl = `${frontendUrl.replace(/\\/$/, '')}${entryPath}?${params.toString()}`;\n    if (typeof window !== 'undefined') {\n      window.location.href = entryUrl;\n    }\n  }\n\n  /**\n   * Check authorization with full result details\n   *\n   * @example\n   * ```ts\n   * const result = await grant.isAuthorized('document', 'update');\n   * if (!result.authorized) {\n   *   console.log('Denied:', result.reason);\n   * }\n   * ```\n   */\n  async isAuthorized(\n    resource: string,\n    action: string,\n    options?: PermissionQueryOptions\n  ): Promise<AuthorizationResult> {\n    const contextResourceKey =\n      options?.context?.resource != null ? JSON.stringify(options.context.resource) : undefined;\n    const cacheKey = this.getCacheKey('auth', resource, action, options?.scope, contextResourceKey);\n\n    // Check cache first (unless explicitly disabled)\n    if (options?.useCache !== false) {\n      const cached = this.getFromCache<AuthorizationResult>(cacheKey);\n      if (cached) return cached;\n    }\n\n    try {\n      // API expects: { permission: { resource, action }, context: { resource?: any }, scope?: { tenant, id } }\n      // scope is optional - for session tokens it enables dynamic scope switching\n      const scope = options?.scope;\n      const hasValidScope =\n        scope && typeof scope === 'object' && 'tenant' in scope && 'id' in scope;\n      // When scope is provided and context.resource is not, derive context.resource from scope.id\n      const contextResource =\n        options?.context?.resource ??\n        (hasValidScope && scope && 'id' in scope && scope.id != null ? { id: scope.id } : null);\n\n      const response = await this.fetchWithAuth('/api/auth/is-authorized', {\n        method: 'POST',\n        body: JSON.stringify({\n          permission: {\n            resource,\n            action,\n          },\n          context: {\n            resource: contextResource,\n          },\n          // Pass scope for dynamic scope override (only works with session tokens)\n          ...(hasValidScope && { scope }),\n        }),\n      });\n\n      if (!response.ok) {\n        const error = await response.json().catch(() => ({}));\n        return {\n          authorized: false,\n          reason: error.message || `API error: ${response.status}`,\n        };\n      }\n\n      const json = await response.json();\n      // API returns { success: true, data: { authorized, ... } }\n      const result: AuthorizationResult = json.data ?? json;\n      this.setCache(cacheKey, result);\n      return result;\n    } catch (error) {\n      return {\n        authorized: false,\n        reason: error instanceof Error ? error.message : 'Unknown error',\n      };\n    }\n  }\n\n  // ============================================================================\n  // Public API - Cache Management\n  // ============================================================================\n\n  /**\n   * Clear all cached data\n   */\n  clearCache(): void {\n    this.cache.clear();\n  }\n\n  /**\n   * Clear cached data for a specific scope\n   */\n  clearScopeCache(scope?: Scope): void {\n    const scopeKey = scope ? JSON.stringify(scope) : 'default';\n    for (const key of this.cache.keys()) {\n      if (key.includes(scopeKey)) {\n        this.cache.delete(key);\n      }\n    }\n  }\n\n  // ============================================================================\n  // Private - HTTP & Authentication\n  // ============================================================================\n\n  /**\n   * Make an authenticated fetch request with automatic token refresh on 401\n   * and MFA step-up on 403 MFA_REQUIRED.\n   */\n  private async fetchWithAuth(url: string, init?: RequestInit): Promise<Response> {\n    const response = await this.doFetch(url, init);\n\n    if (response.status === 403 && this.config.onMfaRequired) {\n      const cloned = response.clone();\n      const body = await cloned.json().catch(() => null);\n      if (body && (body.code === 'MFA_REQUIRED' || body.extensions?.reason === 'MFA_REQUIRED')) {\n        if (!sharedMfaStepUpPromise) {\n          sharedMfaStepUpPromise = this.config.onMfaRequired().finally(() => {\n            sharedMfaStepUpPromise = null;\n          });\n        }\n        const verified = await sharedMfaStepUpPromise;\n        if (verified) return this.doFetch(url, init);\n      }\n    }\n\n    if (response.status !== 401) return response;\n\n    // Cookie-based refresh (HttpOnly refresh cookie). Body-based refresh is not supported.\n    // Module-level shared promise so all 401s (any client instance) coalesce into one refresh.\n    if (this.config.onRefreshWithCredentials) {\n      if (!sharedCredentialsRefreshPromise) {\n        sharedCredentialsRefreshPromise = this.config.onRefreshWithCredentials().finally(() => {\n          sharedCredentialsRefreshPromise = null;\n        });\n      }\n      const refreshed = await sharedCredentialsRefreshPromise;\n      if (refreshed) return this.doFetch(url, init);\n      this.config.onUnauthorized?.();\n    }\n\n    return response;\n  }\n\n  /**\n   * Perform the actual fetch request\n   */\n  private async doFetch(url: string, init?: RequestInit): Promise<Response> {\n    const fetchFn = this.config.fetch ?? globalThis.fetch;\n    const fullUrl = url.startsWith('http') ? url : `${this.config.apiUrl}${url}`;\n\n    const headers: Record<string, string> = {\n      'Content-Type': 'application/json',\n      ...(init?.headers as Record<string, string>),\n    };\n\n    // Add authorization header if token is available\n    const token = await this.getToken();\n    if (token) {\n      headers['Authorization'] = `Bearer ${token}`;\n    }\n\n    return fetchFn(fullUrl, {\n      ...init,\n      headers,\n      // Include cookies for same-origin requests (supports cookie-based auth)\n      credentials: this.config.credentials ?? 'include',\n    });\n  }\n\n  /**\n   * Get the current access token\n   */\n  private async getToken(): Promise<string | null> {\n    if (this.config.getAccessToken) {\n      const token = this.config.getAccessToken();\n      return token instanceof Promise ? token : token;\n    }\n    return null;\n  }\n\n  // ============================================================================\n  // Private - Cache & URL Helpers\n  // ============================================================================\n\n  private buildUrl(path: string, scope?: Scope): string {\n    const url = new URL(path, this.config.apiUrl);\n    if (scope) {\n      url.searchParams.set('scope', JSON.stringify(scope));\n    }\n    return url.toString();\n  }\n\n  private getCacheKey(...parts: (string | Scope | undefined)[]): string {\n    const prefix = this.config.cache?.prefix ?? 'grant';\n    return `${prefix}:${parts.map((p) => (p ? JSON.stringify(p) : 'default')).join(':')}`;\n  }\n\n  private getFromCache<T>(key: string): T | null {\n    const entry = this.cache.get(key);\n    if (!entry) return null;\n\n    if (Date.now() > entry.expires) {\n      this.cache.delete(key);\n      return null;\n    }\n\n    return entry.data as T;\n  }\n\n  private setCache(key: string, data: unknown): void {\n    this.cache.set(key, {\n      data,\n      expires: Date.now() + this.defaultTtl,\n    });\n  }\n}\n"],"mappings":"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"}