@weirdfingers/boards 0.1.4

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.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * No-auth provider for local development without authentication.
3
+ */
4
+
5
+ import { BaseAuthProvider } from './base';
6
+ import { AuthState, User, AuthProviderConfig } from '../types';
7
+
8
+ interface NoAuthConfig extends AuthProviderConfig {
9
+ /**
10
+ * Default user ID for development.
11
+ * Defaults to 'dev-user'.
12
+ */
13
+ defaultUserId?: string;
14
+
15
+ /**
16
+ * Default user email for development.
17
+ * Defaults to 'dev@example.com'.
18
+ */
19
+ defaultEmail?: string;
20
+
21
+ /**
22
+ * Default display name for development.
23
+ * Defaults to 'Development User'.
24
+ */
25
+ defaultDisplayName?: string;
26
+ }
27
+
28
+ export class NoAuthProvider extends BaseAuthProvider {
29
+ protected config: NoAuthConfig;
30
+ private listeners: ((state: AuthState) => void)[] = [];
31
+ private currentState: AuthState;
32
+ private defaultUser: User;
33
+
34
+ constructor(config: NoAuthConfig = {}) {
35
+ super(config);
36
+
37
+ // Production safety check
38
+ const nodeEnv = typeof process !== 'undefined' ? process.env?.NODE_ENV : '';
39
+ const isDevelopment = nodeEnv === 'development' || nodeEnv === '' || nodeEnv === 'test';
40
+
41
+ if (!isDevelopment) {
42
+ const error = new Error(
43
+ 'NoAuthProvider cannot be used in production environments. ' +
44
+ 'Please configure a proper authentication provider (JWT, Supabase, Clerk, etc.)'
45
+ );
46
+ console.error('🚨 SECURITY ERROR:', error.message);
47
+ throw error;
48
+ }
49
+
50
+ this.config = {
51
+ defaultUserId: 'dev-user',
52
+ defaultEmail: 'dev@example.com',
53
+ defaultDisplayName: 'Development User',
54
+ ...config,
55
+ };
56
+
57
+ this.defaultUser = {
58
+ id: this.config.defaultUserId!,
59
+ email: this.config.defaultEmail!,
60
+ name: this.config.defaultDisplayName,
61
+ avatar: undefined,
62
+ metadata: { provider: 'none' },
63
+ credits: {
64
+ balance: 1000,
65
+ reserved: 0,
66
+ },
67
+ };
68
+
69
+ this.currentState = {
70
+ user: this.defaultUser,
71
+ status: 'authenticated', // Always authenticated in no-auth mode
72
+ signIn: this.signIn.bind(this),
73
+ signOut: this.signOut.bind(this),
74
+ getToken: this.getToken.bind(this),
75
+ refreshToken: this.refreshToken.bind(this),
76
+ };
77
+
78
+ // Use structured warning instead of console.warn
79
+ if (console.warn) {
80
+ console.warn(
81
+ '🚨 [AUTH] NoAuthProvider is active - authentication is disabled!',
82
+ {
83
+ message: 'This should ONLY be used in development environments',
84
+ environment: nodeEnv || 'unknown',
85
+ provider: 'none',
86
+ }
87
+ );
88
+ }
89
+ }
90
+
91
+ async initialize(): Promise<void> {
92
+ // No initialization needed - always authenticated
93
+ this.updateState({ user: this.defaultUser, status: 'authenticated' });
94
+ }
95
+
96
+ async getAuthState(): Promise<AuthState> {
97
+ return this.currentState;
98
+ }
99
+
100
+ async signIn(): Promise<void> {
101
+ // No-op in no-auth mode - already signed in
102
+ if (console.info) {
103
+ console.info('[AUTH] SignIn called in no-auth mode - no action taken', {
104
+ provider: 'none',
105
+ action: 'signIn',
106
+ status: 'ignored'
107
+ });
108
+ }
109
+ }
110
+
111
+ async signOut(): Promise<void> {
112
+ // No-op in no-auth mode - can't sign out
113
+ if (console.info) {
114
+ console.info('[AUTH] SignOut called in no-auth mode - no action taken', {
115
+ provider: 'none',
116
+ action: 'signOut',
117
+ status: 'ignored'
118
+ });
119
+ }
120
+ }
121
+
122
+ async getToken(): Promise<string | null> {
123
+ // Return a fake development token
124
+ return 'dev-token|no-auth-mode|always-valid';
125
+ }
126
+
127
+ async refreshToken(): Promise<string | null> {
128
+ // Return the same fake token since it doesn't expire
129
+ return 'dev-token|no-auth-mode|always-valid';
130
+ }
131
+
132
+ async getUser(): Promise<User | null> {
133
+ return this.defaultUser;
134
+ }
135
+
136
+ onAuthStateChange(callback: (state: AuthState) => void): () => void {
137
+ // Call immediately with current state
138
+ callback(this.currentState);
139
+
140
+ this.listeners.push(callback);
141
+ return () => {
142
+ const index = this.listeners.indexOf(callback);
143
+ if (index > -1) {
144
+ this.listeners.splice(index, 1);
145
+ }
146
+ };
147
+ }
148
+
149
+ async destroy(): Promise<void> {
150
+ this.listeners = [];
151
+ }
152
+
153
+ private updateState(updates: Partial<AuthState>): void {
154
+ this.currentState = { ...this.currentState, ...updates };
155
+ this.listeners.forEach(listener => listener(this.currentState));
156
+ }
157
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Core authentication types and interfaces.
3
+ */
4
+
5
+ export interface User {
6
+ id: string;
7
+ email: string;
8
+ name?: string;
9
+ avatar?: string;
10
+ metadata: Record<string, unknown>;
11
+ credits: {
12
+ balance: number;
13
+ reserved: number;
14
+ };
15
+ }
16
+
17
+ export interface AuthProvider {
18
+ id: string;
19
+ name: string;
20
+ type: 'oauth' | 'email' | 'magic-link' | 'custom';
21
+ config: Record<string, unknown>;
22
+ }
23
+
24
+ export interface SignInOptions {
25
+ provider?: string;
26
+ redirectTo?: string;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ export interface AuthState {
31
+ user: User | null;
32
+ status: 'loading' | 'authenticated' | 'unauthenticated' | 'error';
33
+ signIn: (provider?: AuthProvider, options?: SignInOptions) => Promise<void>;
34
+ signOut: () => Promise<void>;
35
+ getToken: () => Promise<string | null>;
36
+ refreshToken: () => Promise<string | null>;
37
+ }
38
+
39
+ export interface AuthProviderConfig {
40
+ /**
41
+ * Tenant identifier for multi-tenant applications.
42
+ * If not provided, defaults to single-tenant mode.
43
+ */
44
+ tenantId?: string;
45
+
46
+ /**
47
+ * Additional configuration specific to the auth provider.
48
+ */
49
+ [key: string]: unknown;
50
+ }
51
+
52
+ export interface AuthContextValue extends AuthState {
53
+ /**
54
+ * Whether the auth system is initializing.
55
+ */
56
+ isInitializing: boolean;
57
+
58
+ /**
59
+ * Any error that occurred during authentication.
60
+ */
61
+ error: Error | null;
62
+
63
+ /**
64
+ * Clear any authentication errors.
65
+ */
66
+ clearError: () => void;
67
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * API configuration context for providing backend URLs to hooks.
3
+ */
4
+
5
+ import { createContext, useContext, ReactNode } from "react";
6
+
7
+ export interface ApiConfig {
8
+ /**
9
+ * Base URL for the backend API (e.g., "http://localhost:8088")
10
+ * Used for REST endpoints like SSE streams
11
+ */
12
+ apiUrl: string;
13
+ /**
14
+ * GraphQL endpoint URL (e.g., "http://localhost:8088/graphql")
15
+ */
16
+ graphqlUrl: string;
17
+ /**
18
+ * WebSocket URL for GraphQL subscriptions
19
+ */
20
+ subscriptionUrl?: string;
21
+ }
22
+
23
+ const ApiConfigContext = createContext<ApiConfig | null>(null);
24
+
25
+ interface ApiConfigProviderProps {
26
+ children: ReactNode;
27
+ config: ApiConfig;
28
+ }
29
+
30
+ export function ApiConfigProvider({
31
+ children,
32
+ config,
33
+ }: ApiConfigProviderProps) {
34
+ return (
35
+ <ApiConfigContext.Provider value={config}>
36
+ {children}
37
+ </ApiConfigContext.Provider>
38
+ );
39
+ }
40
+
41
+ export function useApiConfig(): ApiConfig {
42
+ const context = useContext(ApiConfigContext);
43
+ if (!context) {
44
+ throw new Error("useApiConfig must be used within ApiConfigProvider");
45
+ }
46
+ return context;
47
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * GraphQL client configuration with authentication.
3
+ */
4
+
5
+ import {
6
+ createClient,
7
+ fetchExchange,
8
+ cacheExchange,
9
+ subscriptionExchange,
10
+ makeOperation,
11
+ } from "urql";
12
+ import { authExchange } from "@urql/exchange-auth";
13
+ import { createClient as createWSClient } from "graphql-ws";
14
+
15
+ interface AuthState {
16
+ getToken(): Promise<string | null>;
17
+ }
18
+
19
+ interface ClientConfig {
20
+ url: string;
21
+ subscriptionUrl?: string;
22
+ auth: AuthState;
23
+ tenantId?: string;
24
+ }
25
+
26
+ export function createGraphQLClient({
27
+ url,
28
+ subscriptionUrl,
29
+ auth,
30
+ tenantId,
31
+ }: ClientConfig) {
32
+ const wsClient = subscriptionUrl
33
+ ? createWSClient({
34
+ url: subscriptionUrl,
35
+ connectionParams: async () => {
36
+ const token = await auth.getToken();
37
+ const headers: Record<string, string> = {};
38
+
39
+ if (token) {
40
+ headers.Authorization = `Bearer ${token}`;
41
+ }
42
+
43
+ if (tenantId) {
44
+ headers["X-Tenant"] = tenantId;
45
+ }
46
+
47
+ return headers;
48
+ },
49
+ })
50
+ : null;
51
+
52
+ return createClient({
53
+ url,
54
+ exchanges: [
55
+ cacheExchange,
56
+ authExchange(async () => {
57
+ // Initialize auth state by fetching token
58
+ let token = await auth.getToken();
59
+
60
+ return {
61
+ addAuthToOperation: (operation) => {
62
+ // Build headers
63
+ const headers: Record<string, string> = {};
64
+
65
+ if (token) {
66
+ headers.Authorization = `Bearer ${token}`;
67
+ }
68
+
69
+ if (tenantId) {
70
+ headers["X-Tenant"] = tenantId;
71
+ }
72
+ const fetchOptions =
73
+ typeof operation.context.fetchOptions === "function"
74
+ ? operation.context.fetchOptions()
75
+ : operation.context.fetchOptions || {};
76
+
77
+ // Add headers to operation context
78
+ return makeOperation(operation.kind, operation, {
79
+ ...operation.context,
80
+ fetchOptions: {
81
+ ...operation.context.fetchOptions,
82
+ headers: {
83
+ ...fetchOptions.headers,
84
+ ...headers,
85
+ },
86
+ },
87
+ });
88
+ },
89
+
90
+ didAuthError: (error) => {
91
+ // Check if error is auth-related
92
+ return error.graphQLErrors.some(
93
+ (e) =>
94
+ e.extensions?.code === "UNAUTHENTICATED" ||
95
+ e.extensions?.code === "UNAUTHORIZED"
96
+ );
97
+ },
98
+
99
+ willAuthError: () => {
100
+ // We don't preemptively block requests
101
+ return false;
102
+ },
103
+
104
+ refreshAuth: async () => {
105
+ // Re-fetch token on auth error and update the closure variable
106
+ token = await auth.getToken();
107
+ },
108
+ };
109
+ }),
110
+ fetchExchange,
111
+ ...(wsClient
112
+ ? [
113
+ subscriptionExchange({
114
+ forwardSubscription: (operation) => ({
115
+ subscribe: (sink) => ({
116
+ unsubscribe: wsClient.subscribe(
117
+ {
118
+ query: operation.query || "",
119
+ variables: operation.variables,
120
+ },
121
+ sink
122
+ ),
123
+ }),
124
+ }),
125
+ }),
126
+ ]
127
+ : []),
128
+ ],
129
+ });
130
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * GraphQL queries and mutations for the Boards API.
3
+ */
4
+
5
+ import { gql } from "urql";
6
+
7
+ // Fragments for reusable query parts
8
+ export const USER_FRAGMENT = gql`
9
+ fragment UserFragment on User {
10
+ id
11
+ email
12
+ displayName
13
+ avatarUrl
14
+ createdAt
15
+ }
16
+ `;
17
+
18
+ export const BOARD_FRAGMENT = gql`
19
+ fragment BoardFragment on Board {
20
+ id
21
+ tenantId
22
+ ownerId
23
+ title
24
+ description
25
+ isPublic
26
+ settings
27
+ metadata
28
+ createdAt
29
+ updatedAt
30
+ generationCount
31
+ }
32
+ `;
33
+
34
+ export const GENERATION_FRAGMENT = gql`
35
+ fragment GenerationFragment on Generation {
36
+ id
37
+ boardId
38
+ userId
39
+ generatorName
40
+ artifactType
41
+ status
42
+ progress
43
+ storageUrl
44
+ thumbnailUrl
45
+ inputParams
46
+ outputMetadata
47
+ errorMessage
48
+ createdAt
49
+ updatedAt
50
+ completedAt
51
+ }
52
+ `;
53
+
54
+ // Auth queries
55
+ export const GET_CURRENT_USER = gql`
56
+ ${USER_FRAGMENT}
57
+ query GetCurrentUser {
58
+ me {
59
+ ...UserFragment
60
+ }
61
+ }
62
+ `;
63
+
64
+ // Board queries
65
+ export const GET_BOARDS = gql`
66
+ ${BOARD_FRAGMENT}
67
+ ${USER_FRAGMENT}
68
+ query GetBoards($limit: Int, $offset: Int) {
69
+ myBoards(limit: $limit, offset: $offset) {
70
+ ...BoardFragment
71
+ owner {
72
+ ...UserFragment
73
+ }
74
+ }
75
+ }
76
+ `;
77
+
78
+ export const GET_BOARD = gql`
79
+ ${BOARD_FRAGMENT}
80
+ ${USER_FRAGMENT}
81
+ ${GENERATION_FRAGMENT}
82
+ query GetBoard($id: UUID!) {
83
+ board(id: $id) {
84
+ ...BoardFragment
85
+ owner {
86
+ ...UserFragment
87
+ }
88
+ members {
89
+ id
90
+ boardId
91
+ userId
92
+ role
93
+ invitedBy
94
+ joinedAt
95
+ user {
96
+ ...UserFragment
97
+ }
98
+ inviter {
99
+ ...UserFragment
100
+ }
101
+ }
102
+ generations(limit: 10) {
103
+ ...GenerationFragment
104
+ }
105
+ }
106
+ }
107
+ `;
108
+
109
+ // Generator queries
110
+ export const GET_GENERATORS = gql`
111
+ query GetGenerators($artifactType: String) {
112
+ generators(artifactType: $artifactType) {
113
+ name
114
+ description
115
+ artifactType
116
+ inputSchema
117
+ }
118
+ }
119
+ `;
120
+
121
+ // Generation queries
122
+ export const GET_GENERATIONS = gql`
123
+ ${GENERATION_FRAGMENT}
124
+ query GetGenerations($boardId: UUID, $limit: Int, $offset: Int) {
125
+ generations(boardId: $boardId, limit: $limit, offset: $offset) {
126
+ ...GenerationFragment
127
+ board {
128
+ id
129
+ title
130
+ }
131
+ user {
132
+ ...UserFragment
133
+ }
134
+ }
135
+ }
136
+ `;
137
+
138
+ export const GET_GENERATION = gql`
139
+ ${GENERATION_FRAGMENT}
140
+ query GetGeneration($id: UUID!) {
141
+ generation(id: $id) {
142
+ ...GenerationFragment
143
+ board {
144
+ ...BoardFragment
145
+ }
146
+ user {
147
+ ...UserFragment
148
+ }
149
+ }
150
+ }
151
+ `;
152
+
153
+ // Board mutations
154
+ export const CREATE_BOARD = gql`
155
+ ${BOARD_FRAGMENT}
156
+ ${USER_FRAGMENT}
157
+ mutation CreateBoard($input: CreateBoardInput!) {
158
+ createBoard(input: $input) {
159
+ ...BoardFragment
160
+ owner {
161
+ ...UserFragment
162
+ }
163
+ }
164
+ }
165
+ `;
166
+
167
+ export const UPDATE_BOARD = gql`
168
+ ${BOARD_FRAGMENT}
169
+ mutation UpdateBoard($id: UUID!, $input: UpdateBoardInput!) {
170
+ updateBoard(id: $id, input: $input) {
171
+ ...BoardFragment
172
+ }
173
+ }
174
+ `;
175
+
176
+ export const DELETE_BOARD = gql`
177
+ mutation DeleteBoard($id: UUID!) {
178
+ deleteBoard(id: $id) {
179
+ success
180
+ }
181
+ }
182
+ `;
183
+
184
+ // Board member mutations
185
+ export const ADD_BOARD_MEMBER = gql`
186
+ mutation AddBoardMember($boardId: UUID!, $email: String!, $role: BoardRole!) {
187
+ addBoardMember(boardId: $boardId, email: $email, role: $role) {
188
+ id
189
+ boardId
190
+ userId
191
+ role
192
+ invitedBy
193
+ joinedAt
194
+ user {
195
+ ...UserFragment
196
+ }
197
+ }
198
+ }
199
+ `;
200
+
201
+ export const UPDATE_BOARD_MEMBER_ROLE = gql`
202
+ mutation UpdateBoardMemberRole($id: UUID!, $role: BoardRole!) {
203
+ updateBoardMemberRole(id: $id, role: $role) {
204
+ id
205
+ role
206
+ }
207
+ }
208
+ `;
209
+
210
+ export const REMOVE_BOARD_MEMBER = gql`
211
+ mutation RemoveBoardMember($id: UUID!) {
212
+ removeBoardMember(id: $id) {
213
+ success
214
+ }
215
+ }
216
+ `;
217
+
218
+ // Generation mutations
219
+ export const CREATE_GENERATION = gql`
220
+ ${GENERATION_FRAGMENT}
221
+ mutation CreateGeneration($input: CreateGenerationInput!) {
222
+ createGeneration(input: $input) {
223
+ ...GenerationFragment
224
+ }
225
+ }
226
+ `;
227
+
228
+ export const CANCEL_GENERATION = gql`
229
+ mutation CancelGeneration($id: UUID!) {
230
+ cancelGeneration(id: $id) {
231
+ id
232
+ status
233
+ }
234
+ }
235
+ `;
236
+
237
+ export const RETRY_GENERATION = gql`
238
+ ${GENERATION_FRAGMENT}
239
+ mutation RetryGeneration($id: UUID!) {
240
+ retryGeneration(id: $id) {
241
+ ...GenerationFragment
242
+ }
243
+ }
244
+ `;
245
+
246
+ // Input types (these should match your backend GraphQL schema)
247
+ export interface CreateBoardInput {
248
+ title: string;
249
+ description?: string;
250
+ isPublic?: boolean;
251
+ settings?: Record<string, unknown>;
252
+ metadata?: Record<string, unknown>;
253
+ }
254
+
255
+ export interface UpdateBoardInput {
256
+ title?: string;
257
+ description?: string;
258
+ isPublic?: boolean;
259
+ settings?: Record<string, unknown>;
260
+ metadata?: Record<string, unknown>;
261
+ }
262
+
263
+ export interface CreateGenerationInput {
264
+ boardId: string;
265
+ generatorName: string;
266
+ artifactType: ArtifactType; // Allow string for flexibility with new types
267
+ inputParams: Record<string, unknown>;
268
+ metadata?: Record<string, unknown>;
269
+ }
270
+
271
+ // Enums (should match backend)
272
+ export enum BoardRole {
273
+ VIEWER = "VIEWER",
274
+ EDITOR = "EDITOR",
275
+ ADMIN = "ADMIN",
276
+ }
277
+
278
+ export enum GenerationStatus {
279
+ PENDING = "PENDING",
280
+ RUNNING = "RUNNING",
281
+ COMPLETED = "COMPLETED",
282
+ FAILED = "FAILED",
283
+ CANCELLED = "CANCELLED",
284
+ }
285
+
286
+ export enum ArtifactType {
287
+ IMAGE = "image",
288
+ VIDEO = "video",
289
+ AUDIO = "audio",
290
+ TEXT = "text",
291
+ LORA = "lora",
292
+ MODEL = "model",
293
+ }