@grantjs/client 1.5.3 → 1.6.1

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.
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.142.0/helpers/esm/typeof.js
1
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/typeof.js
2
2
  function _typeof(o) {
3
3
  "@babel/helpers - typeof";
4
4
  return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
@@ -8,7 +8,7 @@ function _typeof(o) {
8
8
  }, _typeof(o);
9
9
  }
10
10
  //#endregion
11
- //#region \0@oxc-project+runtime@0.142.0/helpers/esm/toPrimitive.js
11
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/toPrimitive.js
12
12
  function toPrimitive(t, r) {
13
13
  if ("object" != _typeof(t) || !t) return t;
14
14
  var e = t[Symbol.toPrimitive];
@@ -20,13 +20,13 @@ function toPrimitive(t, r) {
20
20
  return ("string" === r ? String : Number)(t);
21
21
  }
22
22
  //#endregion
23
- //#region \0@oxc-project+runtime@0.142.0/helpers/esm/toPropertyKey.js
23
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/toPropertyKey.js
24
24
  function toPropertyKey(t) {
25
25
  var i = toPrimitive(t, "string");
26
26
  return "symbol" == _typeof(i) ? i : i + "";
27
27
  }
28
28
  //#endregion
29
- //#region \0@oxc-project+runtime@0.142.0/helpers/esm/defineProperty.js
29
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/defineProperty.js
30
30
  function _defineProperty(e, r, t) {
31
31
  return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
32
32
  value: t,
@@ -251,4 +251,4 @@ Object.defineProperty(exports, "GrantClient", {
251
251
  }
252
252
  });
253
253
 
254
- //# sourceMappingURL=grant-client-Big3VpBv.cjs.map
254
+ //# sourceMappingURL=grant-client-B-FvCDOz.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"grant-client-Big3VpBv.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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"}
1
+ {"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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"}
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.142.0/helpers/esm/typeof.js
1
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/typeof.js
2
2
  function _typeof(o) {
3
3
  "@babel/helpers - typeof";
4
4
  return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
@@ -8,7 +8,7 @@ function _typeof(o) {
8
8
  }, _typeof(o);
9
9
  }
10
10
  //#endregion
11
- //#region \0@oxc-project+runtime@0.142.0/helpers/esm/toPrimitive.js
11
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/toPrimitive.js
12
12
  function toPrimitive(t, r) {
13
13
  if ("object" != _typeof(t) || !t) return t;
14
14
  var e = t[Symbol.toPrimitive];
@@ -20,13 +20,13 @@ function toPrimitive(t, r) {
20
20
  return ("string" === r ? String : Number)(t);
21
21
  }
22
22
  //#endregion
23
- //#region \0@oxc-project+runtime@0.142.0/helpers/esm/toPropertyKey.js
23
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/toPropertyKey.js
24
24
  function toPropertyKey(t) {
25
25
  var i = toPrimitive(t, "string");
26
26
  return "symbol" == _typeof(i) ? i : i + "";
27
27
  }
28
28
  //#endregion
29
- //#region \0@oxc-project+runtime@0.142.0/helpers/esm/defineProperty.js
29
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/defineProperty.js
30
30
  function _defineProperty(e, r, t) {
31
31
  return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
32
32
  value: t,
@@ -246,4 +246,4 @@ var GrantClient = class {
246
246
  //#endregion
247
247
  export { GrantClient as t };
248
248
 
249
- //# sourceMappingURL=grant-client-CjdWdV1P.js.map
249
+ //# sourceMappingURL=grant-client-R5ezpOMB.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"grant-client-CjdWdV1P.js","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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"}
1
+ {"version":3,"file":"grant-client-R5ezpOMB.js","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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"}
package/dist/index.cjs CHANGED
@@ -1,3 +1,3 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_grant_client = require("./grant-client-Big3VpBv.cjs");
2
+ const require_grant_client = require("./grant-client-B-FvCDOz.cjs");
3
3
  exports.GrantClient = require_grant_client.GrantClient;
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as GrantClient } from "./grant-client-CjdWdV1P.js";
1
+ import { t as GrantClient } from "./grant-client-R5ezpOMB.js";
2
2
  export { GrantClient };
package/dist/react.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_grant_client = require("./grant-client-Big3VpBv.cjs");
2
+ const require_grant_client = require("./grant-client-B-FvCDOz.cjs");
3
3
  let react = require("react");
4
4
  let react_jsx_runtime = require("react/jsx-runtime");
5
5
  //#region src/react/context.tsx
package/dist/react.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as GrantClient } from "./grant-client-CjdWdV1P.js";
1
+ import { t as GrantClient } from "./grant-client-R5ezpOMB.js";
2
2
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  //#region src/react/context.tsx
package/dist/types.d.ts CHANGED
@@ -1,5 +1,23 @@
1
- import { Scope } from '../../schema/src/index.ts';
2
- export type { Scope, Tenant } from '../../schema/src/index.ts';
1
+ import { AuthorizationReason, AuthorizationResult as SchemaAuthorizationResult, Scope } from '../../schema/src/index.ts';
2
+ export type { Permission, Resource, Scope, Tenant } from '../../schema/src/index.ts';
3
+ /**
4
+ * Result of an authorization check.
5
+ *
6
+ * Derived from `@grantjs/schema`'s codegen'd type rather than redefined, so every field
7
+ * tracks the SDL automatically -- with one deliberate widening. The SDK reports its own
8
+ * transport failures through the same channel the server uses for `AuthorizationReason`:
9
+ * on a network error or a non-OK response it returns
10
+ * `{ authorized: false, reason: 'Unknown error' }`. So the SDK's `reason` is the wire
11
+ * enum OR a locally synthesised message, and the type has to say so.
12
+ *
13
+ * `(string & {})` keeps the union assignable in both directions -- consumers who treated
14
+ * `reason` as a plain `string` still compile -- while giving editors autocomplete on the
15
+ * eight `AuthorizationReason` members. Do not narrow it to the bare enum without a major:
16
+ * that would break every `reason === 'some literal'` comparison downstream.
17
+ */
18
+ export type AuthorizationResult = Omit<SchemaAuthorizationResult, 'reason'> & {
19
+ reason?: AuthorizationReason | (string & {}) | null;
20
+ };
3
21
  /**
4
22
  * Options for project-app OAuth sign-in (redirect only).
5
23
  */
@@ -98,17 +116,6 @@ export interface CacheOptions {
98
116
  */
99
117
  prefix?: string;
100
118
  }
101
- /**
102
- * Result of an authorization check
103
- */
104
- export interface AuthorizationResult {
105
- /** Whether the action is authorized */
106
- authorized: boolean;
107
- /** Human-readable reason for the decision */
108
- reason?: string;
109
- /** The permission that matched (if authorized) */
110
- matchedPermission?: Permission;
111
- }
112
119
  /**
113
120
  * Options for permission queries
114
121
  */
@@ -122,28 +129,6 @@ export interface PermissionQueryOptions {
122
129
  resource?: Record<string, unknown> | null;
123
130
  };
124
131
  }
125
- /**
126
- * Permission entity
127
- */
128
- export interface Permission {
129
- id: string;
130
- name: string;
131
- description?: string | null;
132
- action: string;
133
- resourceId?: string | null;
134
- resource?: Resource | null;
135
- condition?: unknown;
136
- }
137
- /**
138
- * Resource entity
139
- */
140
- export interface Resource {
141
- id: string;
142
- name: string;
143
- slug: string;
144
- description?: string | null;
145
- actions: string[];
146
- }
147
132
  /**
148
133
  * Error response from the API
149
134
  */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,IAAI,yBAAyB,EAChD,KAAK,EACN,MAAM,iBAAiB,CAAC;AAOzB,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE3E;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,yBAAyB,EAAE,QAAQ,CAAC,GAAG;IAC5E,MAAM,CAAC,EAAE,mBAAmB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC;CACrD,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,WAAW,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAE9D;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9D;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAE5B;;;;;;;OAOG;IACH,wBAAwB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAElD;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAEvC;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAErB;;;OAGG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAC;IAEjC;;OAEG;IACH,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,oFAAoF;IACpF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,oCAAoC;IACpC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,oDAAoD;IACpD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wCAAwC;IACxC,OAAO,CAAC,EAAE;QACR,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KAC3C,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grantjs/client",
3
- "version": "1.5.3",
3
+ "version": "1.6.1",
4
4
  "description": "Browser SDK for Grant authorization - React hooks and components for permission-based UI rendering",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -55,23 +55,22 @@
55
55
  "registry": "https://registry.npmjs.org/"
56
56
  },
57
57
  "dependencies": {
58
- "@grantjs/schema": "1.5.3"
58
+ "@grantjs/schema": "1.6.1"
59
59
  },
60
60
  "devDependencies": {
61
- "@tanstack/react-query": "^5.101.2",
62
- "@testing-library/jest-dom": "^6.9.1",
61
+ "@testing-library/jest-dom": "^7.0.1",
63
62
  "@testing-library/react": "^16.0.0",
64
- "@types/node": "^26.1.1",
63
+ "@types/node": "^26.2.0",
65
64
  "@types/react": "^19",
66
65
  "@vitejs/plugin-react": "^6.0.3",
67
- "@vitest/coverage-v8": "^4.1.10",
68
- "eslint": "^10.7.0",
69
- "jsdom": "^29.1.1",
70
- "react": "^19.2.7",
66
+ "@vitest/coverage-v8": "^4.1.11",
67
+ "eslint": "^10.8.1",
68
+ "jsdom": "^30.0.1",
69
+ "react": "^19.2.8",
71
70
  "typescript": "^6.0.3",
72
- "vite": "^8.2.0",
71
+ "vite": "^8.2.1",
73
72
  "vite-plugin-dts": "^5.0.3",
74
- "vitest": "^4.1.10"
73
+ "vitest": "^4.1.11"
75
74
  },
76
75
  "peerDependencies": {
77
76
  "@tanstack/react-query": "^5",