@ram_28/kf-ai-sdk 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ram_28/kf-ai-sdk",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Type-safe, AI-driven SDK for building modern web applications with role-based access control",
5
5
  "author": "Ramprasad",
6
6
  "license": "MIT",
@@ -0,0 +1,277 @@
1
+ // ============================================================
2
+ // AUTH PROVIDER COMPONENT
3
+ // ============================================================
4
+ // React Context Provider for authentication state
5
+
6
+ import React, {
7
+ createContext,
8
+ useContext,
9
+ useCallback,
10
+ useEffect,
11
+ useMemo,
12
+ useState,
13
+ useRef,
14
+ } from "react";
15
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
16
+
17
+ import type {
18
+ AuthContextValue,
19
+ AuthProviderProps,
20
+ AuthStatus,
21
+ UserDetails,
22
+ SessionResponse,
23
+ AuthProviderName,
24
+ LoginOptions,
25
+ LogoutOptions,
26
+ } from "./types";
27
+
28
+ import {
29
+ fetchSession,
30
+ initiateLogin,
31
+ performLogout,
32
+ AuthenticationError,
33
+ } from "./authClient";
34
+ import { getAuthConfig, configureAuth } from "./authConfig";
35
+
36
+ // ============================================================
37
+ // CONTEXT
38
+ // ============================================================
39
+
40
+ const AuthContext = createContext<AuthContextValue | null>(null);
41
+
42
+ const SESSION_QUERY_KEY = ["auth", "session"] as const;
43
+
44
+ // ============================================================
45
+ // PROVIDER COMPONENT
46
+ // ============================================================
47
+
48
+ export function AuthProvider({
49
+ children,
50
+ config: configOverride,
51
+ onAuthChange,
52
+ onError,
53
+ loadingComponent,
54
+ unauthenticatedComponent,
55
+ skipInitialCheck = false,
56
+ }: AuthProviderProps): React.ReactElement {
57
+ const configApplied = useRef(false);
58
+
59
+ if (configOverride && !configApplied.current) {
60
+ configureAuth(configOverride);
61
+ configApplied.current = true;
62
+ }
63
+
64
+ const queryClient = useQueryClient();
65
+ const authConfig = getAuthConfig();
66
+
67
+ // ============================================================
68
+ // SESSION QUERY
69
+ // ============================================================
70
+
71
+ const {
72
+ data: sessionData,
73
+ isLoading,
74
+ error: queryError,
75
+ refetch,
76
+ isFetching,
77
+ } = useQuery<SessionResponse, Error>({
78
+ queryKey: SESSION_QUERY_KEY,
79
+ queryFn: fetchSession,
80
+ enabled: !skipInitialCheck,
81
+ retry: (failureCount, error) => {
82
+ if (error instanceof AuthenticationError) {
83
+ if (error.statusCode === 401 || error.statusCode === 403) {
84
+ return false;
85
+ }
86
+ }
87
+ return failureCount < authConfig.retry.count;
88
+ },
89
+ retryDelay: authConfig.retry.delay,
90
+ staleTime: authConfig.staleTime,
91
+ gcTime: authConfig.staleTime * 2,
92
+ refetchOnWindowFocus: authConfig.refetchOnWindowFocus ?? true,
93
+ refetchOnReconnect: authConfig.refetchOnReconnect ?? true,
94
+ refetchInterval: authConfig.sessionCheckInterval || false,
95
+ });
96
+
97
+ // ============================================================
98
+ // DERIVED STATE
99
+ // ============================================================
100
+
101
+ const [error, setError] = useState<Error | null>(null);
102
+
103
+ const status: AuthStatus = useMemo(() => {
104
+ if (isLoading || isFetching) return "loading";
105
+ if (sessionData?.userDetails) return "authenticated";
106
+ return "unauthenticated";
107
+ }, [isLoading, isFetching, sessionData]);
108
+
109
+ const user: UserDetails | null = sessionData?.userDetails || null;
110
+ const staticBaseUrl: string | null = sessionData?.staticBaseUrl || null;
111
+ const buildId: string | null = sessionData?.buildId || null;
112
+ const isAuthenticated = status === "authenticated";
113
+
114
+ // ============================================================
115
+ // REFS FOR CALLBACKS
116
+ // ============================================================
117
+
118
+ const onAuthChangeRef = useRef(onAuthChange);
119
+ onAuthChangeRef.current = onAuthChange;
120
+
121
+ const onErrorRef = useRef(onError);
122
+ onErrorRef.current = onError;
123
+
124
+ // ============================================================
125
+ // EFFECTS
126
+ // ============================================================
127
+
128
+ useEffect(() => {
129
+ if (!isLoading) {
130
+ onAuthChangeRef.current?.(status, user);
131
+ }
132
+ }, [status, user, isLoading]);
133
+
134
+ useEffect(() => {
135
+ if (queryError) {
136
+ setError(queryError);
137
+ onErrorRef.current?.(queryError);
138
+ }
139
+ }, [queryError]);
140
+
141
+ useEffect(() => {
142
+ if (
143
+ status === "unauthenticated" &&
144
+ authConfig.autoRedirect &&
145
+ !isLoading
146
+ ) {
147
+ initiateLogin();
148
+ }
149
+ }, [status, isLoading, authConfig.autoRedirect]);
150
+
151
+ // ============================================================
152
+ // AUTH OPERATIONS
153
+ // ============================================================
154
+
155
+ const login = useCallback(
156
+ (provider?: AuthProviderName, options?: LoginOptions) => {
157
+ initiateLogin(provider, options);
158
+ },
159
+ []
160
+ );
161
+
162
+ const logout = useCallback(
163
+ async (options?: LogoutOptions) => {
164
+ queryClient.removeQueries({ queryKey: SESSION_QUERY_KEY });
165
+ await performLogout(options);
166
+ },
167
+ [queryClient]
168
+ );
169
+
170
+ const refreshSession = useCallback(async (): Promise<SessionResponse | null> => {
171
+ // Prevent concurrent refreshes - return existing data if already fetching
172
+ if (isFetching) {
173
+ return sessionData || null;
174
+ }
175
+
176
+ try {
177
+ const result = await refetch();
178
+ return result.data || null;
179
+ } catch (err) {
180
+ setError(err as Error);
181
+ return null;
182
+ }
183
+ }, [refetch, isFetching, sessionData]);
184
+
185
+ const hasRole = useCallback(
186
+ (role: string): boolean => {
187
+ return user?.Role === role;
188
+ },
189
+ [user]
190
+ );
191
+
192
+ const hasAnyRole = useCallback(
193
+ (roles: string[]): boolean => {
194
+ return roles.includes(user?.Role || "");
195
+ },
196
+ [user]
197
+ );
198
+
199
+ const clearError = useCallback(() => {
200
+ setError(null);
201
+ }, []);
202
+
203
+ const _forceCheck = useCallback(() => {
204
+ refetch();
205
+ }, [refetch]);
206
+
207
+ // ============================================================
208
+ // CONTEXT VALUE
209
+ // ============================================================
210
+
211
+ const contextValue: AuthContextValue = useMemo(
212
+ () => ({
213
+ user,
214
+ staticBaseUrl,
215
+ buildId,
216
+ status,
217
+ isAuthenticated,
218
+ isLoading,
219
+ login,
220
+ logout,
221
+ refreshSession,
222
+ hasRole,
223
+ hasAnyRole,
224
+ error,
225
+ clearError,
226
+ _forceCheck,
227
+ }),
228
+ [
229
+ user,
230
+ staticBaseUrl,
231
+ buildId,
232
+ status,
233
+ isAuthenticated,
234
+ isLoading,
235
+ login,
236
+ logout,
237
+ refreshSession,
238
+ hasRole,
239
+ hasAnyRole,
240
+ error,
241
+ clearError,
242
+ _forceCheck,
243
+ ]
244
+ );
245
+
246
+ // ============================================================
247
+ // RENDER
248
+ // ============================================================
249
+
250
+ if (status === "loading" && loadingComponent) {
251
+ return <>{loadingComponent}</>;
252
+ }
253
+
254
+ if (
255
+ status === "unauthenticated" &&
256
+ !authConfig.autoRedirect &&
257
+ unauthenticatedComponent
258
+ ) {
259
+ return <>{unauthenticatedComponent}</>;
260
+ }
261
+
262
+ return (
263
+ <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
264
+ );
265
+ }
266
+
267
+ // ============================================================
268
+ // CONTEXT HOOK (internal)
269
+ // ============================================================
270
+
271
+ export function useAuthContext(): AuthContextValue {
272
+ const context = useContext(AuthContext);
273
+ if (!context) {
274
+ throw new Error("useAuth must be used within an AuthProvider");
275
+ }
276
+ return context;
277
+ }
@@ -0,0 +1,175 @@
1
+ // ============================================================
2
+ // AUTH API CLIENT
3
+ // ============================================================
4
+ // Low-level functions for authentication API calls
5
+
6
+ import type {
7
+ SessionResponse,
8
+ AuthProviderName,
9
+ LoginOptions,
10
+ LogoutOptions,
11
+ } from "./types";
12
+ import {
13
+ getAuthBaseUrl,
14
+ getAuthConfig,
15
+ getProviderConfig,
16
+ } from "./authConfig";
17
+ import { getDefaultHeaders } from "../api/client";
18
+
19
+ /**
20
+ * Custom error class for authentication errors
21
+ */
22
+ export class AuthenticationError extends Error {
23
+ public readonly statusCode: number;
24
+
25
+ constructor(message: string, statusCode: number) {
26
+ super(message);
27
+ this.name = "AuthenticationError";
28
+ this.statusCode = statusCode;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Fetch current session from the server
34
+ * Calls the session endpoint (default: /api/id)
35
+ *
36
+ * @throws AuthenticationError if session check fails or user is not authenticated
37
+ */
38
+ export async function fetchSession(): Promise<SessionResponse> {
39
+ const config = getAuthConfig();
40
+ const baseUrl = getAuthBaseUrl();
41
+ const headers = getDefaultHeaders();
42
+
43
+ const response = await fetch(`${baseUrl}${config.sessionEndpoint}`, {
44
+ method: "GET",
45
+ headers,
46
+ credentials: "include",
47
+ });
48
+
49
+ if (!response.ok) {
50
+ if (response.status === 401 || response.status === 403) {
51
+ throw new AuthenticationError("Not authenticated", response.status);
52
+ }
53
+ throw new AuthenticationError(
54
+ `Session check failed: ${response.statusText}`,
55
+ response.status
56
+ );
57
+ }
58
+
59
+ const data: SessionResponse = await response.json();
60
+ return data;
61
+ }
62
+
63
+ /**
64
+ * Initiates OAuth login flow by redirecting to the auth provider.
65
+ *
66
+ * @remarks
67
+ * This function redirects the browser and never resolves.
68
+ * Any code after calling this function will not execute.
69
+ *
70
+ * @param provider - OAuth provider to use (defaults to config.defaultProvider)
71
+ * @param options - Login options including callback URL and custom params
72
+ * @returns Promise that never resolves (browser redirects away)
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * // Correct usage - no code after login()
77
+ * function handleLoginClick() {
78
+ * login('google');
79
+ * // Don't put code here - it won't run
80
+ * }
81
+ * ```
82
+ */
83
+ export function initiateLogin(
84
+ provider?: AuthProviderName,
85
+ options?: LoginOptions
86
+ ): Promise<never> {
87
+ return new Promise(() => {
88
+ const config = getAuthConfig();
89
+ const baseUrl = getAuthBaseUrl();
90
+
91
+ // Validate base URL
92
+ if (!baseUrl) {
93
+ throw new Error(
94
+ 'Auth base URL is not configured. Call setApiBaseUrl("https://...") or configureAuth({ baseUrl: "https://..." }) first.'
95
+ );
96
+ }
97
+
98
+ const selectedProvider = provider || config.defaultProvider;
99
+ const providerConfig = getProviderConfig(selectedProvider);
100
+
101
+ // Validate provider config
102
+ if (!providerConfig) {
103
+ const availableProviders = Object.keys(config.providers || {}).join(", ") || "none";
104
+ throw new Error(
105
+ `Auth provider "${selectedProvider}" is not configured. Available providers: ${availableProviders}`
106
+ );
107
+ }
108
+
109
+ // Validate login path
110
+ if (!providerConfig.loginPath) {
111
+ throw new Error(
112
+ `Login path not configured for provider "${selectedProvider}". ` +
113
+ `Configure it with: configureAuth({ providers: { ${selectedProvider}: { loginPath: '/api/auth/...' } } })`
114
+ );
115
+ }
116
+
117
+ // Validate URL construction
118
+ let loginUrl: URL;
119
+ try {
120
+ loginUrl = new URL(`${baseUrl}${providerConfig.loginPath}`);
121
+ } catch {
122
+ throw new Error(
123
+ `Failed to construct login URL. Base URL: "${baseUrl}", Login path: "${providerConfig.loginPath}". ` +
124
+ `Ensure baseUrl is a valid URL (e.g., "https://example.com").`
125
+ );
126
+ }
127
+
128
+ if (options?.callbackUrl || config.callbackUrl) {
129
+ loginUrl.searchParams.set(
130
+ "callbackUrl",
131
+ options?.callbackUrl || config.callbackUrl || window.location.href
132
+ );
133
+ }
134
+
135
+ if (options?.params) {
136
+ Object.entries(options.params).forEach(([key, value]) => {
137
+ loginUrl.searchParams.set(key, value);
138
+ });
139
+ }
140
+
141
+ window.location.href = loginUrl.toString();
142
+ // Promise never resolves - browser navigates away
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Logout the current user
148
+ * Optionally calls the logout endpoint before clearing client state
149
+ */
150
+ export async function performLogout(options?: LogoutOptions): Promise<void> {
151
+ const config = getAuthConfig();
152
+ const baseUrl = getAuthBaseUrl();
153
+ const headers = getDefaultHeaders();
154
+
155
+ const providerConfig = getProviderConfig(config.defaultProvider);
156
+ const logoutPath = providerConfig?.logoutPath;
157
+
158
+ if (logoutPath && options?.callLogoutEndpoint !== false) {
159
+ try {
160
+ await fetch(`${baseUrl}${logoutPath}`, {
161
+ method: "POST",
162
+ headers,
163
+ credentials: "include",
164
+ });
165
+ } catch (error) {
166
+ console.warn("Logout endpoint call failed:", error);
167
+ }
168
+ }
169
+
170
+ if (options?.redirectUrl) {
171
+ window.location.href = options.redirectUrl;
172
+ } else if (config.loginRedirectUrl) {
173
+ window.location.href = config.loginRedirectUrl;
174
+ }
175
+ }
@@ -0,0 +1,105 @@
1
+ // ============================================================
2
+ // AUTH CONFIGURATION
3
+ // ============================================================
4
+ // Global auth configuration following the setApiBaseUrl pattern
5
+
6
+ import type { AuthConfig, AuthProviderName, AuthEndpointConfig } from "./types";
7
+ import { getApiBaseUrl } from "../api/client";
8
+
9
+ /**
10
+ * Default auth configuration
11
+ */
12
+ const defaultAuthConfig: AuthConfig = {
13
+ sessionEndpoint: "/api/id",
14
+ providers: {
15
+ google: {
16
+ loginPath: "/api/auth/google/login",
17
+ logoutPath: "/api/auth/logout",
18
+ },
19
+ },
20
+ defaultProvider: "google",
21
+ autoRedirect: true,
22
+ sessionCheckInterval: 0,
23
+ retry: {
24
+ count: 3,
25
+ delay: 1000,
26
+ },
27
+ staleTime: 5 * 60 * 1000,
28
+ refetchOnWindowFocus: true,
29
+ refetchOnReconnect: true,
30
+ };
31
+
32
+ /**
33
+ * Current auth configuration (mutable)
34
+ */
35
+ let authConfig: AuthConfig = { ...defaultAuthConfig };
36
+
37
+ /**
38
+ * Configure authentication settings globally
39
+ * @example
40
+ * ```ts
41
+ * configureAuth({
42
+ * defaultProvider: "google",
43
+ * autoRedirect: true,
44
+ * providers: {
45
+ * google: { loginPath: "/api/auth/google/login" },
46
+ * microsoft: { loginPath: "/api/auth/microsoft/login" },
47
+ * },
48
+ * });
49
+ * ```
50
+ */
51
+ export function configureAuth(config: Partial<AuthConfig>): void {
52
+ authConfig = {
53
+ ...authConfig,
54
+ ...config,
55
+ providers: {
56
+ ...authConfig.providers,
57
+ ...config.providers,
58
+ },
59
+ retry: {
60
+ ...authConfig.retry,
61
+ ...config.retry,
62
+ },
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Add or update an auth provider configuration
68
+ */
69
+ export function setAuthProvider(
70
+ provider: AuthProviderName,
71
+ config: AuthEndpointConfig
72
+ ): void {
73
+ authConfig.providers[provider] = config;
74
+ }
75
+
76
+ /**
77
+ * Get current auth configuration
78
+ */
79
+ export function getAuthConfig(): Readonly<AuthConfig> {
80
+ return { ...authConfig };
81
+ }
82
+
83
+ /**
84
+ * Get the base URL for auth endpoints
85
+ * Falls back to API base URL if not explicitly set
86
+ */
87
+ export function getAuthBaseUrl(): string {
88
+ return authConfig.baseUrl || getApiBaseUrl();
89
+ }
90
+
91
+ /**
92
+ * Get endpoint configuration for a specific provider
93
+ */
94
+ export function getProviderConfig(
95
+ provider: AuthProviderName
96
+ ): AuthEndpointConfig | undefined {
97
+ return authConfig.providers[provider];
98
+ }
99
+
100
+ /**
101
+ * Reset auth configuration to defaults
102
+ */
103
+ export function resetAuthConfig(): void {
104
+ authConfig = { ...defaultAuthConfig };
105
+ }
@@ -0,0 +1,40 @@
1
+ // ============================================================
2
+ // AUTH MODULE EXPORTS
3
+ // ============================================================
4
+
5
+ // Provider component
6
+ export { AuthProvider } from "./AuthProvider";
7
+
8
+ // Main hook
9
+ export { useAuth } from "./useAuth";
10
+
11
+ // Configuration functions
12
+ export {
13
+ configureAuth,
14
+ setAuthProvider,
15
+ getAuthConfig,
16
+ getAuthBaseUrl,
17
+ resetAuthConfig,
18
+ } from "./authConfig";
19
+
20
+ // API client functions (for advanced use cases)
21
+ export {
22
+ fetchSession,
23
+ initiateLogin,
24
+ performLogout,
25
+ AuthenticationError,
26
+ } from "./authClient";
27
+
28
+ // Type exports
29
+ export type {
30
+ UserDetails,
31
+ SessionResponse,
32
+ AuthStatus,
33
+ AuthConfig,
34
+ AuthProviderName,
35
+ AuthEndpointConfig,
36
+ AuthProviderProps,
37
+ UseAuthReturn,
38
+ LoginOptions,
39
+ LogoutOptions,
40
+ } from "./types";