@grantjs/client 1.0.0

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.
package/dist/index.cjs ADDED
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
6
+ let sharedCredentialsRefreshPromise = null;
7
+ class GrantClient {
8
+ constructor(config) {
9
+ __publicField(this, "config");
10
+ __publicField(this, "cache", /* @__PURE__ */ new Map());
11
+ __publicField(this, "defaultTtl");
12
+ this.config = config;
13
+ this.defaultTtl = config.cache?.ttl ?? 5 * 60 * 1e3;
14
+ }
15
+ // ============================================================================
16
+ // Public API - Permission Checks
17
+ // ============================================================================
18
+ /**
19
+ * Check if the current user has a specific permission
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const canEdit = await grant.can('document', 'update');
24
+ * if (canEdit) {
25
+ * // Show edit button
26
+ * }
27
+ * ```
28
+ */
29
+ async can(resource, action, options) {
30
+ const result = await this.isAuthorized(resource, action, options);
31
+ return result.authorized;
32
+ }
33
+ /**
34
+ * Alias for `can` - check if user has permission
35
+ */
36
+ async hasPermission(resource, action, options) {
37
+ return this.can(resource, action, options);
38
+ }
39
+ // ============================================================================
40
+ // Public API - Project OAuth (sign-in with project app)
41
+ // ============================================================================
42
+ /**
43
+ * Start project-app OAuth flow (redirect only).
44
+ * Navigates the current window to the Grant OAuth entry page; after sign-in and consent,
45
+ * the user is redirected to the app's `redirect_uri` with token in the URL fragment.
46
+ *
47
+ * Requires `config.frontendUrl` and `redirectUri`.
48
+ */
49
+ async signInWithProjectApp(options) {
50
+ const frontendUrl = this.config.frontendUrl;
51
+ if (!frontendUrl) {
52
+ throw new Error("GrantClient: frontendUrl is required for signInWithProjectApp");
53
+ }
54
+ const locale = options.locale ?? "en";
55
+ const redirectUri = options.redirectUri;
56
+ if (!redirectUri) {
57
+ throw new Error("redirectUri is required for signInWithProjectApp");
58
+ }
59
+ const entryPath = `/${locale}/auth/project`;
60
+ const params = new URLSearchParams({
61
+ client_id: options.clientId,
62
+ redirect_uri: redirectUri,
63
+ state: options.state ?? ""
64
+ });
65
+ if (options.scope) params.set("scope", options.scope);
66
+ const entryUrl = `${frontendUrl.replace(/\/$/, "")}${entryPath}?${params.toString()}`;
67
+ if (typeof window !== "undefined") {
68
+ window.location.href = entryUrl;
69
+ }
70
+ }
71
+ /**
72
+ * Check authorization with full result details
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * const result = await grant.isAuthorized('document', 'update');
77
+ * if (!result.authorized) {
78
+ * console.log('Denied:', result.reason);
79
+ * }
80
+ * ```
81
+ */
82
+ async isAuthorized(resource, action, options) {
83
+ const contextResourceKey = options?.context?.resource != null ? JSON.stringify(options.context.resource) : void 0;
84
+ const cacheKey = this.getCacheKey("auth", resource, action, options?.scope, contextResourceKey);
85
+ if (options?.useCache !== false) {
86
+ const cached = this.getFromCache(cacheKey);
87
+ if (cached) return cached;
88
+ }
89
+ try {
90
+ const scope = options?.scope;
91
+ const hasValidScope = scope && typeof scope === "object" && "tenant" in scope && "id" in scope;
92
+ const contextResource = options?.context?.resource ?? (hasValidScope && scope && "id" in scope && scope.id != null ? { id: scope.id } : null);
93
+ const response = await this.fetchWithAuth("/api/auth/is-authorized", {
94
+ method: "POST",
95
+ body: JSON.stringify({
96
+ permission: {
97
+ resource,
98
+ action
99
+ },
100
+ context: {
101
+ resource: contextResource
102
+ },
103
+ // Pass scope for dynamic scope override (only works with session tokens)
104
+ ...hasValidScope && { scope }
105
+ })
106
+ });
107
+ if (!response.ok) {
108
+ const error = await response.json().catch(() => ({}));
109
+ return {
110
+ authorized: false,
111
+ reason: error.message || `API error: ${response.status}`
112
+ };
113
+ }
114
+ const json = await response.json();
115
+ const result = json.data ?? json;
116
+ this.setCache(cacheKey, result);
117
+ return result;
118
+ } catch (error) {
119
+ return {
120
+ authorized: false,
121
+ reason: error instanceof Error ? error.message : "Unknown error"
122
+ };
123
+ }
124
+ }
125
+ // ============================================================================
126
+ // Public API - Cache Management
127
+ // ============================================================================
128
+ /**
129
+ * Clear all cached data
130
+ */
131
+ clearCache() {
132
+ this.cache.clear();
133
+ }
134
+ /**
135
+ * Clear cached data for a specific scope
136
+ */
137
+ clearScopeCache(scope) {
138
+ const scopeKey = scope ? JSON.stringify(scope) : "default";
139
+ for (const key of this.cache.keys()) {
140
+ if (key.includes(scopeKey)) {
141
+ this.cache.delete(key);
142
+ }
143
+ }
144
+ }
145
+ // ============================================================================
146
+ // Private - HTTP & Authentication
147
+ // ============================================================================
148
+ /**
149
+ * Make an authenticated fetch request with automatic token refresh on 401
150
+ */
151
+ async fetchWithAuth(url, init) {
152
+ const response = await this.doFetch(url, init);
153
+ if (response.status !== 401) return response;
154
+ if (this.config.onRefreshWithCredentials) {
155
+ if (!sharedCredentialsRefreshPromise) {
156
+ sharedCredentialsRefreshPromise = this.config.onRefreshWithCredentials().finally(() => {
157
+ sharedCredentialsRefreshPromise = null;
158
+ });
159
+ }
160
+ const refreshed = await sharedCredentialsRefreshPromise;
161
+ if (refreshed) return this.doFetch(url, init);
162
+ this.config.onUnauthorized?.();
163
+ }
164
+ return response;
165
+ }
166
+ /**
167
+ * Perform the actual fetch request
168
+ */
169
+ async doFetch(url, init) {
170
+ const fetchFn = this.config.fetch ?? globalThis.fetch;
171
+ const fullUrl = url.startsWith("http") ? url : `${this.config.apiUrl}${url}`;
172
+ const headers = {
173
+ "Content-Type": "application/json",
174
+ ...init?.headers
175
+ };
176
+ const token = await this.getToken();
177
+ if (token) {
178
+ headers["Authorization"] = `Bearer ${token}`;
179
+ }
180
+ return fetchFn(fullUrl, {
181
+ ...init,
182
+ headers,
183
+ // Include cookies for same-origin requests (supports cookie-based auth)
184
+ credentials: this.config.credentials ?? "include"
185
+ });
186
+ }
187
+ /**
188
+ * Get the current access token
189
+ */
190
+ async getToken() {
191
+ if (this.config.getAccessToken) {
192
+ const token = this.config.getAccessToken();
193
+ return token instanceof Promise ? token : token;
194
+ }
195
+ return null;
196
+ }
197
+ // ============================================================================
198
+ // Private - Cache & URL Helpers
199
+ // ============================================================================
200
+ buildUrl(path, scope) {
201
+ const url = new URL(path, this.config.apiUrl);
202
+ if (scope) {
203
+ url.searchParams.set("scope", JSON.stringify(scope));
204
+ }
205
+ return url.toString();
206
+ }
207
+ getCacheKey(...parts) {
208
+ const prefix = this.config.cache?.prefix ?? "grant";
209
+ return `${prefix}:${parts.map((p) => p ? JSON.stringify(p) : "default").join(":")}`;
210
+ }
211
+ getFromCache(key) {
212
+ const entry = this.cache.get(key);
213
+ if (!entry) return null;
214
+ if (Date.now() > entry.expires) {
215
+ this.cache.delete(key);
216
+ return null;
217
+ }
218
+ return entry.data;
219
+ }
220
+ setCache(key, data) {
221
+ this.cache.set(key, {
222
+ data,
223
+ expires: Date.now() + this.defaultTtl
224
+ });
225
+ }
226
+ }
227
+ exports.GrantClient = GrantClient;
228
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/grant-client.ts"],"sourcesContent":["import type {\n GrantClientConfig,\n AuthorizationResult,\n PermissionQueryOptions,\n Scope,\n SignInWithProjectAppOptions,\n} from './types';\n\n/**\n * Module-level shared promise for cookie-only 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 * 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 */\n private async fetchWithAuth(url: string, init?: RequestInit): Promise<Response> {\n const response = await this.doFetch(url, init);\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"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { GrantClient } from './grant-client';
2
+ export type { GrantClientConfig, AuthTokens, CacheOptions, AuthorizationResult, PermissionQueryOptions, Permission, Resource, ApiError, SignInWithProjectAppOptions, Scope, Tenant, } from './types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,YAAY,EACV,iBAAiB,EACjB,UAAU,EACV,YAAY,EACZ,mBAAmB,EACnB,sBAAsB,EACtB,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,2BAA2B,EAE3B,KAAK,EACL,MAAM,GACP,MAAM,SAAS,CAAC"}
package/dist/index.mjs ADDED
@@ -0,0 +1,228 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ let sharedCredentialsRefreshPromise = null;
5
+ class GrantClient {
6
+ constructor(config) {
7
+ __publicField(this, "config");
8
+ __publicField(this, "cache", /* @__PURE__ */ new Map());
9
+ __publicField(this, "defaultTtl");
10
+ this.config = config;
11
+ this.defaultTtl = config.cache?.ttl ?? 5 * 60 * 1e3;
12
+ }
13
+ // ============================================================================
14
+ // Public API - Permission Checks
15
+ // ============================================================================
16
+ /**
17
+ * Check if the current user has a specific permission
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const canEdit = await grant.can('document', 'update');
22
+ * if (canEdit) {
23
+ * // Show edit button
24
+ * }
25
+ * ```
26
+ */
27
+ async can(resource, action, options) {
28
+ const result = await this.isAuthorized(resource, action, options);
29
+ return result.authorized;
30
+ }
31
+ /**
32
+ * Alias for `can` - check if user has permission
33
+ */
34
+ async hasPermission(resource, action, options) {
35
+ return this.can(resource, action, options);
36
+ }
37
+ // ============================================================================
38
+ // Public API - Project OAuth (sign-in with project app)
39
+ // ============================================================================
40
+ /**
41
+ * Start project-app OAuth flow (redirect only).
42
+ * Navigates the current window to the Grant OAuth entry page; after sign-in and consent,
43
+ * the user is redirected to the app's `redirect_uri` with token in the URL fragment.
44
+ *
45
+ * Requires `config.frontendUrl` and `redirectUri`.
46
+ */
47
+ async signInWithProjectApp(options) {
48
+ const frontendUrl = this.config.frontendUrl;
49
+ if (!frontendUrl) {
50
+ throw new Error("GrantClient: frontendUrl is required for signInWithProjectApp");
51
+ }
52
+ const locale = options.locale ?? "en";
53
+ const redirectUri = options.redirectUri;
54
+ if (!redirectUri) {
55
+ throw new Error("redirectUri is required for signInWithProjectApp");
56
+ }
57
+ const entryPath = `/${locale}/auth/project`;
58
+ const params = new URLSearchParams({
59
+ client_id: options.clientId,
60
+ redirect_uri: redirectUri,
61
+ state: options.state ?? ""
62
+ });
63
+ if (options.scope) params.set("scope", options.scope);
64
+ const entryUrl = `${frontendUrl.replace(/\/$/, "")}${entryPath}?${params.toString()}`;
65
+ if (typeof window !== "undefined") {
66
+ window.location.href = entryUrl;
67
+ }
68
+ }
69
+ /**
70
+ * Check authorization with full result details
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const result = await grant.isAuthorized('document', 'update');
75
+ * if (!result.authorized) {
76
+ * console.log('Denied:', result.reason);
77
+ * }
78
+ * ```
79
+ */
80
+ async isAuthorized(resource, action, options) {
81
+ const contextResourceKey = options?.context?.resource != null ? JSON.stringify(options.context.resource) : void 0;
82
+ const cacheKey = this.getCacheKey("auth", resource, action, options?.scope, contextResourceKey);
83
+ if (options?.useCache !== false) {
84
+ const cached = this.getFromCache(cacheKey);
85
+ if (cached) return cached;
86
+ }
87
+ try {
88
+ const scope = options?.scope;
89
+ const hasValidScope = scope && typeof scope === "object" && "tenant" in scope && "id" in scope;
90
+ const contextResource = options?.context?.resource ?? (hasValidScope && scope && "id" in scope && scope.id != null ? { id: scope.id } : null);
91
+ const response = await this.fetchWithAuth("/api/auth/is-authorized", {
92
+ method: "POST",
93
+ body: JSON.stringify({
94
+ permission: {
95
+ resource,
96
+ action
97
+ },
98
+ context: {
99
+ resource: contextResource
100
+ },
101
+ // Pass scope for dynamic scope override (only works with session tokens)
102
+ ...hasValidScope && { scope }
103
+ })
104
+ });
105
+ if (!response.ok) {
106
+ const error = await response.json().catch(() => ({}));
107
+ return {
108
+ authorized: false,
109
+ reason: error.message || `API error: ${response.status}`
110
+ };
111
+ }
112
+ const json = await response.json();
113
+ const result = json.data ?? json;
114
+ this.setCache(cacheKey, result);
115
+ return result;
116
+ } catch (error) {
117
+ return {
118
+ authorized: false,
119
+ reason: error instanceof Error ? error.message : "Unknown error"
120
+ };
121
+ }
122
+ }
123
+ // ============================================================================
124
+ // Public API - Cache Management
125
+ // ============================================================================
126
+ /**
127
+ * Clear all cached data
128
+ */
129
+ clearCache() {
130
+ this.cache.clear();
131
+ }
132
+ /**
133
+ * Clear cached data for a specific scope
134
+ */
135
+ clearScopeCache(scope) {
136
+ const scopeKey = scope ? JSON.stringify(scope) : "default";
137
+ for (const key of this.cache.keys()) {
138
+ if (key.includes(scopeKey)) {
139
+ this.cache.delete(key);
140
+ }
141
+ }
142
+ }
143
+ // ============================================================================
144
+ // Private - HTTP & Authentication
145
+ // ============================================================================
146
+ /**
147
+ * Make an authenticated fetch request with automatic token refresh on 401
148
+ */
149
+ async fetchWithAuth(url, init) {
150
+ const response = await this.doFetch(url, init);
151
+ if (response.status !== 401) return response;
152
+ if (this.config.onRefreshWithCredentials) {
153
+ if (!sharedCredentialsRefreshPromise) {
154
+ sharedCredentialsRefreshPromise = this.config.onRefreshWithCredentials().finally(() => {
155
+ sharedCredentialsRefreshPromise = null;
156
+ });
157
+ }
158
+ const refreshed = await sharedCredentialsRefreshPromise;
159
+ if (refreshed) return this.doFetch(url, init);
160
+ this.config.onUnauthorized?.();
161
+ }
162
+ return response;
163
+ }
164
+ /**
165
+ * Perform the actual fetch request
166
+ */
167
+ async doFetch(url, init) {
168
+ const fetchFn = this.config.fetch ?? globalThis.fetch;
169
+ const fullUrl = url.startsWith("http") ? url : `${this.config.apiUrl}${url}`;
170
+ const headers = {
171
+ "Content-Type": "application/json",
172
+ ...init?.headers
173
+ };
174
+ const token = await this.getToken();
175
+ if (token) {
176
+ headers["Authorization"] = `Bearer ${token}`;
177
+ }
178
+ return fetchFn(fullUrl, {
179
+ ...init,
180
+ headers,
181
+ // Include cookies for same-origin requests (supports cookie-based auth)
182
+ credentials: this.config.credentials ?? "include"
183
+ });
184
+ }
185
+ /**
186
+ * Get the current access token
187
+ */
188
+ async getToken() {
189
+ if (this.config.getAccessToken) {
190
+ const token = this.config.getAccessToken();
191
+ return token instanceof Promise ? token : token;
192
+ }
193
+ return null;
194
+ }
195
+ // ============================================================================
196
+ // Private - Cache & URL Helpers
197
+ // ============================================================================
198
+ buildUrl(path, scope) {
199
+ const url = new URL(path, this.config.apiUrl);
200
+ if (scope) {
201
+ url.searchParams.set("scope", JSON.stringify(scope));
202
+ }
203
+ return url.toString();
204
+ }
205
+ getCacheKey(...parts) {
206
+ const prefix = this.config.cache?.prefix ?? "grant";
207
+ return `${prefix}:${parts.map((p) => p ? JSON.stringify(p) : "default").join(":")}`;
208
+ }
209
+ getFromCache(key) {
210
+ const entry = this.cache.get(key);
211
+ if (!entry) return null;
212
+ if (Date.now() > entry.expires) {
213
+ this.cache.delete(key);
214
+ return null;
215
+ }
216
+ return entry.data;
217
+ }
218
+ setCache(key, data) {
219
+ this.cache.set(key, {
220
+ data,
221
+ expires: Date.now() + this.defaultTtl
222
+ });
223
+ }
224
+ }
225
+ export {
226
+ GrantClient
227
+ };
228
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../src/grant-client.ts"],"sourcesContent":["import type {\n GrantClientConfig,\n AuthorizationResult,\n PermissionQueryOptions,\n Scope,\n SignInWithProjectAppOptions,\n} from './types';\n\n/**\n * Module-level shared promise for cookie-only 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 * 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 */\n private async fetchWithAuth(url: string, init?: RequestInit): Promise<Response> {\n const response = await this.doFetch(url, init);\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"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ import { ReactNode } from 'react';
2
+ import { UseGrantOptions } from '../hooks/useGrant';
3
+ /**
4
+ * Props for the GrantGate component
5
+ */
6
+ export interface GrantGateProps extends UseGrantOptions {
7
+ /** The resource slug to check permission for */
8
+ resource: string;
9
+ /** The action to check */
10
+ action: string;
11
+ /** Content to render if permission is granted */
12
+ children: ReactNode;
13
+ /** Content to render if permission is denied (optional) */
14
+ fallback?: ReactNode;
15
+ /** Content to render while loading (optional) */
16
+ loading?: ReactNode;
17
+ }
18
+ /**
19
+ * Component that conditionally renders children based on permissions
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * // Basic usage - hide element if no permission
24
+ * <GrantGate resource="document" action="update">
25
+ * <EditButton />
26
+ * </GrantGate>
27
+ *
28
+ * // With fallback for denied access
29
+ * <GrantGate
30
+ * resource="admin"
31
+ * action="access"
32
+ * fallback={<p>You don't have admin access</p>}
33
+ * >
34
+ * <AdminPanel />
35
+ * </GrantGate>
36
+ *
37
+ * // With loading state
38
+ * <GrantGate
39
+ * resource="report"
40
+ * action="view"
41
+ * loading={<Spinner />}
42
+ * fallback={<AccessDenied />}
43
+ * >
44
+ * <ReportViewer />
45
+ * </GrantGate>
46
+ *
47
+ * // With scope for multi-tenant
48
+ * <GrantGate
49
+ * resource="project"
50
+ * action="delete"
51
+ * scope={{ tenant: 'project', id: projectId }}
52
+ * >
53
+ * <DeleteProjectButton />
54
+ * </GrantGate>
55
+ * ```
56
+ */
57
+ export declare function GrantGate({ resource, action, scope, enabled, useCache, children, fallback, loading, }: GrantGateProps): ReactNode;
58
+ //# sourceMappingURL=GrantGate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GrantGate.d.ts","sourceRoot":"","sources":["../../../src/react/components/GrantGate.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAY,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEnE;;GAEG;AACH,MAAM,WAAW,cAAe,SAAQ,eAAe;IACrD,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,QAAQ,EAAE,SAAS,CAAC;IACpB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,iDAAiD;IACjD,OAAO,CAAC,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,SAAS,CAAC,EACxB,QAAQ,EACR,MAAM,EACN,KAAK,EACL,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAe,EACf,OAAc,GACf,EAAE,cAAc,GAAG,SAAS,CA+B5B"}
@@ -0,0 +1,73 @@
1
+ import { ReactNode } from 'react';
2
+ import { GrantClient } from '../grant-client';
3
+ import { GrantClientConfig } from '../types';
4
+ /**
5
+ * Props for the GrantProvider component
6
+ */
7
+ export interface GrantProviderProps {
8
+ /**
9
+ * Grant client configuration
10
+ */
11
+ config: GrantClientConfig;
12
+ /**
13
+ * Pre-configured GrantClient instance (alternative to config)
14
+ * If provided, config is ignored
15
+ */
16
+ client?: GrantClient;
17
+ /**
18
+ * Child components
19
+ */
20
+ children: ReactNode;
21
+ }
22
+ /**
23
+ * Provider component that makes the Grant client available to child components
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * // Option 1: Pass config (cookie-based refresh)
28
+ * <GrantProvider
29
+ * config={{
30
+ * apiUrl: 'https://api.grant.com',
31
+ * getAccessToken: () => localStorage.getItem('accessToken'),
32
+ * onRefreshWithCredentials: async () => {
33
+ * const res = await fetch('https://api.grant.com/api/auth/refresh', { method: 'POST', credentials: 'include' });
34
+ * if (!res.ok) return false;
35
+ * const { data } = await res.json();
36
+ * if (data?.accessToken) { localStorage.setItem('accessToken', data.accessToken); return true; }
37
+ * return false;
38
+ * },
39
+ * onTokenRefresh: (tokens) => { localStorage.setItem('accessToken', tokens.accessToken); },
40
+ * onUnauthorized: () => { window.location.href = '/login'; },
41
+ * }}
42
+ * >
43
+ * <App />
44
+ * </GrantProvider>
45
+ *
46
+ * // Option 2: Pass pre-configured client
47
+ * const grant = new GrantClient({ ... });
48
+ * <GrantProvider client={grant}>
49
+ * <App />
50
+ * </GrantProvider>
51
+ * ```
52
+ */
53
+ export declare function GrantProvider({ config, client, children }: GrantProviderProps): import("react/jsx-runtime").JSX.Element;
54
+ /**
55
+ * Hook to access the Grant client from context
56
+ *
57
+ * @throws Error if used outside of GrantProvider
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * const grant = useGrantClient();
62
+ * const hasPermission = await grant.can('resource', 'action');
63
+ * ```
64
+ */
65
+ export declare function useGrantClient(): GrantClient;
66
+ /**
67
+ * Hook to optionally access the Grant client
68
+ * Returns null if not in a GrantProvider context
69
+ *
70
+ * Use this when you want to gracefully handle missing provider
71
+ */
72
+ export declare function useGrantClientOptional(): GrantClient | null;
73
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/react/context.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE3E,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAOlD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,MAAM,EAAE,iBAAiB,CAAC;IAE1B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,kBAAkB,2CAO7E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,IAAI,WAAW,CAW5C;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,WAAW,GAAG,IAAI,CAE3D"}