@ram_28/kf-ai-sdk 1.0.1 → 1.0.2

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.2",
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,271 @@
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: true,
93
+ refetchInterval: authConfig.sessionCheckInterval || false,
94
+ });
95
+
96
+ // ============================================================
97
+ // DERIVED STATE
98
+ // ============================================================
99
+
100
+ const [error, setError] = useState<Error | null>(null);
101
+
102
+ const status: AuthStatus = useMemo(() => {
103
+ if (isLoading || isFetching) return "loading";
104
+ if (sessionData?.userDetails) return "authenticated";
105
+ return "unauthenticated";
106
+ }, [isLoading, isFetching, sessionData]);
107
+
108
+ const user: UserDetails | null = sessionData?.userDetails || null;
109
+ const staticBaseUrl: string | null = sessionData?.staticBaseUrl || null;
110
+ const buildId: string | null = sessionData?.buildId || null;
111
+ const isAuthenticated = status === "authenticated";
112
+
113
+ // ============================================================
114
+ // REFS FOR CALLBACKS
115
+ // ============================================================
116
+
117
+ const onAuthChangeRef = useRef(onAuthChange);
118
+ onAuthChangeRef.current = onAuthChange;
119
+
120
+ const onErrorRef = useRef(onError);
121
+ onErrorRef.current = onError;
122
+
123
+ // ============================================================
124
+ // EFFECTS
125
+ // ============================================================
126
+
127
+ useEffect(() => {
128
+ if (!isLoading) {
129
+ onAuthChangeRef.current?.(status, user);
130
+ }
131
+ }, [status, user, isLoading]);
132
+
133
+ useEffect(() => {
134
+ if (queryError) {
135
+ setError(queryError);
136
+ onErrorRef.current?.(queryError);
137
+ }
138
+ }, [queryError]);
139
+
140
+ useEffect(() => {
141
+ if (
142
+ status === "unauthenticated" &&
143
+ authConfig.autoRedirect &&
144
+ !isLoading
145
+ ) {
146
+ initiateLogin();
147
+ }
148
+ }, [status, isLoading, authConfig.autoRedirect]);
149
+
150
+ // ============================================================
151
+ // AUTH OPERATIONS
152
+ // ============================================================
153
+
154
+ const login = useCallback(
155
+ (provider?: AuthProviderName, options?: LoginOptions) => {
156
+ initiateLogin(provider, options);
157
+ },
158
+ []
159
+ );
160
+
161
+ const logout = useCallback(
162
+ async (options?: LogoutOptions) => {
163
+ queryClient.removeQueries({ queryKey: SESSION_QUERY_KEY });
164
+ await performLogout(options);
165
+ },
166
+ [queryClient]
167
+ );
168
+
169
+ const refreshSession = useCallback(async (): Promise<SessionResponse | null> => {
170
+ try {
171
+ const result = await refetch();
172
+ return result.data || null;
173
+ } catch (err) {
174
+ setError(err as Error);
175
+ return null;
176
+ }
177
+ }, [refetch]);
178
+
179
+ const hasRole = useCallback(
180
+ (role: string): boolean => {
181
+ return user?.Role === role;
182
+ },
183
+ [user]
184
+ );
185
+
186
+ const hasAnyRole = useCallback(
187
+ (roles: string[]): boolean => {
188
+ return roles.includes(user?.Role || "");
189
+ },
190
+ [user]
191
+ );
192
+
193
+ const clearError = useCallback(() => {
194
+ setError(null);
195
+ }, []);
196
+
197
+ const _forceCheck = useCallback(() => {
198
+ refetch();
199
+ }, [refetch]);
200
+
201
+ // ============================================================
202
+ // CONTEXT VALUE
203
+ // ============================================================
204
+
205
+ const contextValue: AuthContextValue = useMemo(
206
+ () => ({
207
+ user,
208
+ staticBaseUrl,
209
+ buildId,
210
+ status,
211
+ isAuthenticated,
212
+ isLoading,
213
+ login,
214
+ logout,
215
+ refreshSession,
216
+ hasRole,
217
+ hasAnyRole,
218
+ error,
219
+ clearError,
220
+ _forceCheck,
221
+ }),
222
+ [
223
+ user,
224
+ staticBaseUrl,
225
+ buildId,
226
+ status,
227
+ isAuthenticated,
228
+ isLoading,
229
+ login,
230
+ logout,
231
+ refreshSession,
232
+ hasRole,
233
+ hasAnyRole,
234
+ error,
235
+ clearError,
236
+ _forceCheck,
237
+ ]
238
+ );
239
+
240
+ // ============================================================
241
+ // RENDER
242
+ // ============================================================
243
+
244
+ if (status === "loading" && loadingComponent) {
245
+ return <>{loadingComponent}</>;
246
+ }
247
+
248
+ if (
249
+ status === "unauthenticated" &&
250
+ !authConfig.autoRedirect &&
251
+ unauthenticatedComponent
252
+ ) {
253
+ return <>{unauthenticatedComponent}</>;
254
+ }
255
+
256
+ return (
257
+ <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
258
+ );
259
+ }
260
+
261
+ // ============================================================
262
+ // CONTEXT HOOK (internal)
263
+ // ============================================================
264
+
265
+ export function useAuthContext(): AuthContextValue {
266
+ const context = useContext(AuthContext);
267
+ if (!context) {
268
+ throw new Error("useAuth must be used within an AuthProvider");
269
+ }
270
+ return context;
271
+ }
@@ -0,0 +1,127 @@
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
+ * Initiate login flow by redirecting to the auth provider
65
+ * The server handles the OAuth flow and sets cookies
66
+ */
67
+ export function initiateLogin(
68
+ provider?: AuthProviderName,
69
+ options?: LoginOptions
70
+ ): void {
71
+ const config = getAuthConfig();
72
+ const baseUrl = getAuthBaseUrl();
73
+ const selectedProvider = provider || config.defaultProvider;
74
+
75
+ const providerConfig = getProviderConfig(selectedProvider);
76
+ if (!providerConfig) {
77
+ throw new Error(`Auth provider "${selectedProvider}" is not configured`);
78
+ }
79
+
80
+ const loginUrl = new URL(`${baseUrl}${providerConfig.loginPath}`);
81
+
82
+ if (options?.callbackUrl || config.callbackUrl) {
83
+ loginUrl.searchParams.set(
84
+ "callbackUrl",
85
+ options?.callbackUrl || config.callbackUrl || window.location.href
86
+ );
87
+ }
88
+
89
+ if (options?.params) {
90
+ Object.entries(options.params).forEach(([key, value]) => {
91
+ loginUrl.searchParams.set(key, value);
92
+ });
93
+ }
94
+
95
+ window.location.href = loginUrl.toString();
96
+ }
97
+
98
+ /**
99
+ * Logout the current user
100
+ * Optionally calls the logout endpoint before clearing client state
101
+ */
102
+ export async function performLogout(options?: LogoutOptions): Promise<void> {
103
+ const config = getAuthConfig();
104
+ const baseUrl = getAuthBaseUrl();
105
+ const headers = getDefaultHeaders();
106
+
107
+ const providerConfig = getProviderConfig(config.defaultProvider);
108
+ const logoutPath = providerConfig?.logoutPath;
109
+
110
+ if (logoutPath && options?.callLogoutEndpoint !== false) {
111
+ try {
112
+ await fetch(`${baseUrl}${logoutPath}`, {
113
+ method: "POST",
114
+ headers,
115
+ credentials: "include",
116
+ });
117
+ } catch (error) {
118
+ console.warn("Logout endpoint call failed:", error);
119
+ }
120
+ }
121
+
122
+ if (options?.redirectUrl) {
123
+ window.location.href = options.redirectUrl;
124
+ } else if (config.loginRedirectUrl) {
125
+ window.location.href = config.loginRedirectUrl;
126
+ }
127
+ }
@@ -0,0 +1,103 @@
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
+ };
29
+
30
+ /**
31
+ * Current auth configuration (mutable)
32
+ */
33
+ let authConfig: AuthConfig = { ...defaultAuthConfig };
34
+
35
+ /**
36
+ * Configure authentication settings globally
37
+ * @example
38
+ * ```ts
39
+ * configureAuth({
40
+ * defaultProvider: "google",
41
+ * autoRedirect: true,
42
+ * providers: {
43
+ * google: { loginPath: "/api/auth/google/login" },
44
+ * microsoft: { loginPath: "/api/auth/microsoft/login" },
45
+ * },
46
+ * });
47
+ * ```
48
+ */
49
+ export function configureAuth(config: Partial<AuthConfig>): void {
50
+ authConfig = {
51
+ ...authConfig,
52
+ ...config,
53
+ providers: {
54
+ ...authConfig.providers,
55
+ ...config.providers,
56
+ },
57
+ retry: {
58
+ ...authConfig.retry,
59
+ ...config.retry,
60
+ },
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Add or update an auth provider configuration
66
+ */
67
+ export function setAuthProvider(
68
+ provider: AuthProviderName,
69
+ config: AuthEndpointConfig
70
+ ): void {
71
+ authConfig.providers[provider] = config;
72
+ }
73
+
74
+ /**
75
+ * Get current auth configuration
76
+ */
77
+ export function getAuthConfig(): Readonly<AuthConfig> {
78
+ return { ...authConfig };
79
+ }
80
+
81
+ /**
82
+ * Get the base URL for auth endpoints
83
+ * Falls back to API base URL if not explicitly set
84
+ */
85
+ export function getAuthBaseUrl(): string {
86
+ return authConfig.baseUrl || getApiBaseUrl();
87
+ }
88
+
89
+ /**
90
+ * Get endpoint configuration for a specific provider
91
+ */
92
+ export function getProviderConfig(
93
+ provider: AuthProviderName
94
+ ): AuthEndpointConfig | undefined {
95
+ return authConfig.providers[provider];
96
+ }
97
+
98
+ /**
99
+ * Reset auth configuration to defaults
100
+ */
101
+ export function resetAuthConfig(): void {
102
+ authConfig = { ...defaultAuthConfig };
103
+ }
@@ -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";