reflet-sdk 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to `reflet-sdk` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-01-30
9
+
10
+ ### Added
11
+
12
+ - Initial release of the Reflet SDK
13
+ - **Core Client** (`reflet-sdk`)
14
+ - `Reflet` class for direct API interaction
15
+ - Full TypeScript support with comprehensive types
16
+ - Methods: `list`, `get`, `create`, `vote`, `comment`, `subscribe`, `unsubscribe`
17
+ - Board configuration and roadmap/changelog fetching
18
+ - Custom error classes: `RefletError`, `RefletAuthError`, `RefletNotFoundError`, `RefletValidationError`
19
+
20
+ - **React Bindings** (`reflet-sdk/react`)
21
+ - `RefletProvider` for app-wide configuration
22
+ - Query hooks: `useFeedbackList`, `useFeedback`, `useComments`, `useBoardConfig`, `useRoadmap`, `useChangelog`
23
+ - Mutation hooks: `useCreateFeedback`, `useVote`, `useAddComment`, `useSubscription`
24
+ - Built-in loading, error, and refetch states
25
+
26
+ - **Server Utilities** (`reflet-sdk/server`)
27
+ - `signUser` function for secure JWT token generation
28
+ - `verifyUser` function for token validation
29
+ - Server-side user signing for production security
package/README.md ADDED
@@ -0,0 +1,342 @@
1
+ # reflet-sdk
2
+
3
+ Official SDK for integrating Reflet feedback collection into your application. Collect user feedback, feature requests, and bug reports directly from your app.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install reflet-sdk
9
+ # or
10
+ yarn add reflet-sdk
11
+ # or
12
+ pnpm add reflet-sdk
13
+ # or
14
+ bun add reflet-sdk
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ### 1. Get Your API Keys
20
+
21
+ Go to your Reflet dashboard → Board Settings → API Keys & Widget to generate your keys:
22
+
23
+ - **Public Key** (`fb_pub_...`) - Safe to use in frontend code
24
+ - **Secret Key** (`fb_sec_...`) - Keep secure, use only on your server
25
+
26
+ ### 2. Basic Usage (Vanilla JavaScript/TypeScript)
27
+
28
+ ```typescript
29
+ import { Reflet } from 'reflet-sdk';
30
+
31
+ const reflet = new Reflet({
32
+ publicKey: 'fb_pub_your_key_here',
33
+ user: {
34
+ id: 'user_123',
35
+ email: 'jane@example.com',
36
+ name: 'Jane Doe',
37
+ },
38
+ });
39
+
40
+ // List feedback
41
+ const { items } = await reflet.list({ status: 'open' });
42
+
43
+ // Get single feedback with comments
44
+ const feedback = await reflet.get('feedback_id');
45
+
46
+ // Create new feedback
47
+ const { feedbackId } = await reflet.create({
48
+ title: 'Dark mode support',
49
+ description: 'Would love to have a dark theme option!',
50
+ });
51
+
52
+ // Vote on feedback
53
+ await reflet.vote('feedback_id');
54
+
55
+ // Add a comment
56
+ await reflet.comment({
57
+ feedbackId: 'feedback_id',
58
+ body: 'This would be amazing!',
59
+ });
60
+ ```
61
+
62
+ ### 3. React Usage
63
+
64
+ ```tsx
65
+ import { RefletProvider, useFeedbackList, useVote, useCreateFeedback } from 'reflet-sdk/react';
66
+
67
+ // Wrap your app with the provider
68
+ function App() {
69
+ const currentUser = useCurrentUser(); // Your auth system
70
+
71
+ return (
72
+ <RefletProvider
73
+ publicKey="fb_pub_your_key_here"
74
+ user={currentUser}
75
+ >
76
+ <FeedbackPage />
77
+ </RefletProvider>
78
+ );
79
+ }
80
+
81
+ // Use hooks in your components
82
+ function FeedbackPage() {
83
+ const { data, isLoading, error } = useFeedbackList({ sortBy: 'votes' });
84
+ const { mutate: vote } = useVote();
85
+ const { mutate: createFeedback } = useCreateFeedback();
86
+
87
+ if (isLoading) return <div>Loading...</div>;
88
+ if (error) return <div>Error: {error.message}</div>;
89
+
90
+ return (
91
+ <div>
92
+ {data?.items.map((item) => (
93
+ <div key={item.id}>
94
+ <h3>{item.title}</h3>
95
+ <p>{item.description}</p>
96
+ <button onClick={() => vote({ feedbackId: item.id })}>
97
+ {item.hasVoted ? 'Voted' : 'Vote'} ({item.voteCount})
98
+ </button>
99
+ </div>
100
+ ))}
101
+ </div>
102
+ );
103
+ }
104
+ ```
105
+
106
+ ### 4. Server-Side User Signing (Most Secure)
107
+
108
+ For production apps, sign user tokens on your server to prevent spoofing:
109
+
110
+ ```typescript
111
+ // On your server (Next.js API route, Express, etc.)
112
+ import { signUser } from 'reflet-sdk/server';
113
+
114
+ export async function getUserToken(user: User) {
115
+ const { token, expiresAt } = signUser(
116
+ {
117
+ id: user.id,
118
+ email: user.email,
119
+ name: user.name,
120
+ avatar: user.avatarUrl,
121
+ },
122
+ process.env.REFLET_SECRET_KEY!
123
+ );
124
+
125
+ return { token, expiresAt };
126
+ }
127
+ ```
128
+
129
+ ```tsx
130
+ // On the client
131
+ import { Reflet } from 'reflet-sdk';
132
+
133
+ const reflet = new Reflet({
134
+ publicKey: 'fb_pub_your_key_here',
135
+ userToken: tokenFromServer, // JWT from your server
136
+ });
137
+ ```
138
+
139
+ ## API Reference
140
+
141
+ ### `Reflet` Client
142
+
143
+ ```typescript
144
+ const reflet = new Reflet(config: RefletConfig);
145
+ ```
146
+
147
+ #### Configuration
148
+
149
+ | Option | Type | Required | Description |
150
+ |--------|------|----------|-------------|
151
+ | `publicKey` | `string` | Yes | Your board's public API key |
152
+ | `user` | `RefletUser` | No | User information for identification |
153
+ | `userToken` | `string` | No | Signed JWT from your server (alternative to `user`) |
154
+ | `baseUrl` | `string` | No | Custom API endpoint |
155
+
156
+ #### Methods
157
+
158
+ | Method | Description |
159
+ |--------|-------------|
160
+ | `list(params?)` | List feedback with optional filtering |
161
+ | `get(feedbackId)` | Get single feedback with full details |
162
+ | `create(params)` | Submit new feedback |
163
+ | `vote(feedbackId, type?)` | Vote on feedback (toggle) |
164
+ | `comment(params)` | Add a comment |
165
+ | `subscribe(feedbackId)` | Subscribe to updates |
166
+ | `unsubscribe(feedbackId)` | Unsubscribe from updates |
167
+ | `getConfig()` | Get board configuration |
168
+ | `getRoadmap()` | Get roadmap data |
169
+ | `getChangelog(limit?)` | Get changelog entries |
170
+
171
+ ### React Hooks
172
+
173
+ | Hook | Description |
174
+ |------|-------------|
175
+ | `useFeedbackList(options?)` | Fetch feedback list with filters |
176
+ | `useFeedback(feedbackId)` | Fetch single feedback item |
177
+ | `useComments(feedbackId)` | Fetch comments for feedback |
178
+ | `useVote()` | Vote mutation hook |
179
+ | `useCreateFeedback()` | Create feedback mutation hook |
180
+ | `useAddComment()` | Add comment mutation hook |
181
+ | `useSubscription()` | Subscribe/unsubscribe hooks |
182
+ | `useBoardConfig()` | Fetch board configuration |
183
+ | `useRoadmap()` | Fetch roadmap data |
184
+ | `useChangelog(limit?)` | Fetch changelog entries |
185
+
186
+ ### Server Utilities
187
+
188
+ ```typescript
189
+ import { signUser, verifyUser } from 'reflet-sdk/server';
190
+
191
+ // Sign a user token
192
+ const { token, expiresAt } = signUser(user, secretKey, expiresInSeconds?);
193
+
194
+ // Verify a token (optional - server-side validation)
195
+ const user = verifyUser(token, secretKey);
196
+ ```
197
+
198
+ ## Types
199
+
200
+ ```typescript
201
+ interface RefletUser {
202
+ id: string;
203
+ email?: string;
204
+ name?: string;
205
+ avatar?: string;
206
+ }
207
+
208
+ interface FeedbackItem {
209
+ id: string;
210
+ title: string;
211
+ description: string;
212
+ status: FeedbackStatus;
213
+ voteCount: number;
214
+ commentCount: number;
215
+ hasVoted: boolean;
216
+ author?: FeedbackAuthor;
217
+ tags: FeedbackTag[];
218
+ createdAt: string;
219
+ }
220
+
221
+ interface FeedbackDetail extends FeedbackItem {
222
+ isSubscribed: boolean;
223
+ comments: Comment[];
224
+ }
225
+
226
+ type FeedbackStatus =
227
+ | 'open'
228
+ | 'under_review'
229
+ | 'planned'
230
+ | 'in_progress'
231
+ | 'completed'
232
+ | 'closed';
233
+ ```
234
+
235
+ ## Error Handling
236
+
237
+ The SDK provides typed errors for better error handling:
238
+
239
+ ```typescript
240
+ import { RefletError, RefletAuthError, RefletNotFoundError } from 'reflet-sdk';
241
+
242
+ try {
243
+ await reflet.vote('invalid_id');
244
+ } catch (error) {
245
+ if (error instanceof RefletNotFoundError) {
246
+ console.log('Feedback not found');
247
+ } else if (error instanceof RefletAuthError) {
248
+ console.log('Authentication required');
249
+ } else if (error instanceof RefletError) {
250
+ console.log('API error:', error.message);
251
+ }
252
+ }
253
+ ```
254
+
255
+ ## Examples
256
+
257
+ ### Feedback Widget Component
258
+
259
+ ```tsx
260
+ import { RefletProvider, useFeedbackList, useCreateFeedback, useVote } from 'reflet-sdk/react';
261
+ import { useState } from 'react';
262
+
263
+ function FeedbackWidget() {
264
+ const [isOpen, setIsOpen] = useState(false);
265
+ const { data, refetch } = useFeedbackList({ limit: 5, sortBy: 'votes' });
266
+ const { mutate: createFeedback, isLoading } = useCreateFeedback();
267
+ const { mutate: vote } = useVote();
268
+ const [title, setTitle] = useState('');
269
+
270
+ const handleSubmit = async (e: React.FormEvent) => {
271
+ e.preventDefault();
272
+ await createFeedback({ title, description: '' });
273
+ setTitle('');
274
+ refetch();
275
+ };
276
+
277
+ return (
278
+ <div className="feedback-widget">
279
+ <button onClick={() => setIsOpen(!isOpen)}>
280
+ Feedback
281
+ </button>
282
+
283
+ {isOpen && (
284
+ <div className="feedback-panel">
285
+ <form onSubmit={handleSubmit}>
286
+ <input
287
+ value={title}
288
+ onChange={(e) => setTitle(e.target.value)}
289
+ placeholder="What's on your mind?"
290
+ />
291
+ <button type="submit" disabled={isLoading}>
292
+ Submit
293
+ </button>
294
+ </form>
295
+
296
+ <ul>
297
+ {data?.items.map((item) => (
298
+ <li key={item.id}>
299
+ <button onClick={() => vote({ feedbackId: item.id })}>
300
+ {item.voteCount}
301
+ </button>
302
+ <span>{item.title}</span>
303
+ </li>
304
+ ))}
305
+ </ul>
306
+ </div>
307
+ )}
308
+ </div>
309
+ );
310
+ }
311
+ ```
312
+
313
+ ### Next.js App Router Integration
314
+
315
+ ```tsx
316
+ // app/providers.tsx
317
+ 'use client';
318
+
319
+ import { RefletProvider } from 'reflet-sdk/react';
320
+ import { useSession } from 'next-auth/react';
321
+
322
+ export function Providers({ children }: { children: React.ReactNode }) {
323
+ const { data: session } = useSession();
324
+
325
+ return (
326
+ <RefletProvider
327
+ publicKey={process.env.NEXT_PUBLIC_REFLET_KEY!}
328
+ user={session?.user ? {
329
+ id: session.user.id,
330
+ email: session.user.email!,
331
+ name: session.user.name!,
332
+ } : undefined}
333
+ >
334
+ {children}
335
+ </RefletProvider>
336
+ );
337
+ }
338
+ ```
339
+
340
+ ## License
341
+
342
+ MIT
@@ -0,0 +1,241 @@
1
+ // src/types.ts
2
+ var RefletError = class extends Error {
3
+ status;
4
+ code;
5
+ constructor(message, status, code) {
6
+ super(message);
7
+ this.name = "RefletError";
8
+ this.status = status;
9
+ this.code = code;
10
+ }
11
+ };
12
+ var RefletAuthError = class extends RefletError {
13
+ constructor(message) {
14
+ super(message, 401, "AUTH_ERROR");
15
+ this.name = "RefletAuthError";
16
+ }
17
+ };
18
+ var RefletValidationError = class extends RefletError {
19
+ constructor(message) {
20
+ super(message, 400, "VALIDATION_ERROR");
21
+ this.name = "RefletValidationError";
22
+ }
23
+ };
24
+ var RefletNotFoundError = class extends RefletError {
25
+ constructor(message) {
26
+ super(message, 404, "NOT_FOUND");
27
+ this.name = "RefletNotFoundError";
28
+ }
29
+ };
30
+
31
+ // src/client.ts
32
+ var DEFAULT_BASE_URL = "https://grateful-butterfly-1.convex.cloud";
33
+ var Reflet = class {
34
+ publicKey;
35
+ baseUrl;
36
+ userToken;
37
+ user;
38
+ constructor(config) {
39
+ this.publicKey = config.publicKey;
40
+ this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
41
+ this.userToken = config.userToken;
42
+ this.user = config.user;
43
+ if (!this.publicKey) {
44
+ throw new Error("Reflet: publicKey is required");
45
+ }
46
+ }
47
+ /**
48
+ * Update the user identification
49
+ */
50
+ setUser(user) {
51
+ this.user = user;
52
+ this.userToken = void 0;
53
+ }
54
+ /**
55
+ * Set the user token directly (for server-signed tokens)
56
+ */
57
+ setUserToken(token) {
58
+ this.userToken = token;
59
+ this.user = void 0;
60
+ }
61
+ /**
62
+ * Get the board configuration
63
+ */
64
+ async getConfig() {
65
+ return await this.request("GET", "/api/v1/feedback");
66
+ }
67
+ /**
68
+ * List feedback items with optional filtering
69
+ */
70
+ async list(params) {
71
+ const searchParams = new URLSearchParams();
72
+ if (params?.statusId) {
73
+ searchParams.set("statusId", params.statusId);
74
+ }
75
+ if (params?.status) {
76
+ searchParams.set("status", params.status);
77
+ }
78
+ if (params?.search) {
79
+ searchParams.set("search", params.search);
80
+ }
81
+ if (params?.sortBy) {
82
+ searchParams.set("sortBy", params.sortBy);
83
+ }
84
+ if (params?.limit) {
85
+ searchParams.set("limit", String(params.limit));
86
+ }
87
+ if (params?.offset) {
88
+ searchParams.set("offset", String(params.offset));
89
+ }
90
+ const query = searchParams.toString();
91
+ const url = `/api/v1/feedback/list${query ? `?${query}` : ""}`;
92
+ return await this.request("GET", url);
93
+ }
94
+ /**
95
+ * Get a single feedback item by ID
96
+ */
97
+ async get(feedbackId) {
98
+ const result = await this.request(
99
+ "GET",
100
+ `/api/v1/feedback/item?id=${encodeURIComponent(feedbackId)}`
101
+ );
102
+ if (!result) {
103
+ throw new RefletNotFoundError("Feedback not found");
104
+ }
105
+ return result;
106
+ }
107
+ /**
108
+ * Create new feedback
109
+ * Requires user identification
110
+ */
111
+ async create(params) {
112
+ return await this.request(
113
+ "POST",
114
+ "/api/v1/feedback/create",
115
+ params
116
+ );
117
+ }
118
+ /**
119
+ * Toggle vote on feedback
120
+ * Requires user identification
121
+ */
122
+ async vote(feedbackId, type = "upvote") {
123
+ return await this.request("POST", "/api/v1/feedback/vote", {
124
+ feedbackId,
125
+ voteType: type
126
+ });
127
+ }
128
+ /**
129
+ * Get comments for a feedback item
130
+ */
131
+ async getComments(feedbackId, sortBy = "oldest") {
132
+ return await this.request(
133
+ "GET",
134
+ `/api/v1/feedback/comments?feedbackId=${encodeURIComponent(feedbackId)}&sortBy=${sortBy}`
135
+ );
136
+ }
137
+ /**
138
+ * Add a comment to feedback
139
+ * Requires user identification
140
+ */
141
+ async comment(params) {
142
+ return await this.request(
143
+ "POST",
144
+ "/api/v1/feedback/comment",
145
+ params
146
+ );
147
+ }
148
+ /**
149
+ * Subscribe to feedback updates
150
+ * Requires user identification
151
+ */
152
+ async subscribe(feedbackId) {
153
+ return await this.request(
154
+ "POST",
155
+ "/api/v1/feedback/subscribe",
156
+ {
157
+ feedbackId
158
+ }
159
+ );
160
+ }
161
+ /**
162
+ * Unsubscribe from feedback updates
163
+ * Requires user identification
164
+ */
165
+ async unsubscribe(feedbackId) {
166
+ return await this.request(
167
+ "POST",
168
+ "/api/v1/feedback/unsubscribe",
169
+ {
170
+ feedbackId
171
+ }
172
+ );
173
+ }
174
+ /**
175
+ * Get roadmap data
176
+ */
177
+ async getRoadmap() {
178
+ return await this.request("GET", "/api/v1/feedback/roadmap");
179
+ }
180
+ /**
181
+ * Get changelog entries
182
+ */
183
+ async getChangelog(limit) {
184
+ const url = limit ? `/api/v1/feedback/changelog?limit=${limit}` : "/api/v1/feedback/changelog";
185
+ return await this.request("GET", url);
186
+ }
187
+ /**
188
+ * Make an authenticated API request
189
+ */
190
+ async request(method, path, body) {
191
+ const headers = {
192
+ "Content-Type": "application/json",
193
+ Authorization: `Bearer ${this.publicKey}`
194
+ };
195
+ const token = this.userToken ?? this.generateUserToken();
196
+ if (token) {
197
+ headers["X-User-Token"] = token;
198
+ }
199
+ const response = await fetch(`${this.baseUrl}${path}`, {
200
+ method,
201
+ headers,
202
+ body: body ? JSON.stringify(body) : void 0
203
+ });
204
+ const data = await response.json();
205
+ if (!response.ok) {
206
+ const errorMessage = data.error ?? "Request failed";
207
+ if (response.status === 401) {
208
+ throw new RefletAuthError(errorMessage);
209
+ }
210
+ if (response.status === 404) {
211
+ throw new RefletNotFoundError(errorMessage);
212
+ }
213
+ throw new RefletError(errorMessage, response.status);
214
+ }
215
+ return data;
216
+ }
217
+ /**
218
+ * Generate a simple user token for client-side use
219
+ * Note: For production, use server-side token signing
220
+ */
221
+ generateUserToken() {
222
+ if (!this.user) {
223
+ return void 0;
224
+ }
225
+ const payload = {
226
+ id: this.user.id,
227
+ email: this.user.email,
228
+ name: this.user.name,
229
+ iat: Math.floor(Date.now() / 1e3),
230
+ exp: Math.floor(Date.now() / 1e3) + 86400
231
+ // 24 hours
232
+ };
233
+ const header = btoa(JSON.stringify({ alg: "none", typ: "JWT" }));
234
+ const payloadB64 = btoa(JSON.stringify(payload));
235
+ return `${header}.${payloadB64}.`;
236
+ }
237
+ };
238
+
239
+ export { Reflet, RefletAuthError, RefletError, RefletNotFoundError, RefletValidationError };
240
+ //# sourceMappingURL=chunk-RDQP4VCA.js.map
241
+ //# sourceMappingURL=chunk-RDQP4VCA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/client.ts"],"names":[],"mappings":"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","file":"chunk-RDQP4VCA.js","sourcesContent":["/**\n * Reflet SDK Types\n */\n\n// ============================================\n// Configuration\n// ============================================\n\nexport interface RefletConfig {\n /** Your board's public API key (fb_pub_xxx) */\n publicKey: string;\n /** API base URL (defaults to https://api.reflet.app) */\n baseUrl?: string;\n /** User identification for SSO */\n user?: RefletUser;\n /** Pre-signed user token (alternative to user object) */\n userToken?: string;\n}\n\nexport interface RefletUser {\n /** Unique identifier for the user in your system */\n id: string;\n /** User's email address */\n email?: string;\n /** User's display name */\n name?: string;\n /** URL to user's avatar image */\n avatar?: string;\n /** Additional metadata */\n metadata?: Record<string, unknown>;\n}\n\n// ============================================\n// API Responses\n// ============================================\n\nexport interface BoardConfig {\n id: string;\n name: string;\n slug: string;\n description?: string;\n isPublic: boolean;\n settings?: BoardSettings;\n organization: {\n id: string;\n name: string;\n slug: string;\n logo?: string;\n primaryColor?: string;\n };\n statuses: BoardStatus[];\n}\n\nexport interface BoardSettings {\n allowAnonymousVoting?: boolean;\n requireApproval?: boolean;\n defaultStatus?: FeedbackStatus;\n}\n\nexport interface BoardStatus {\n id: string;\n name: string;\n color: string;\n icon?: string;\n order: number;\n}\n\nexport type FeedbackStatus =\n | \"open\"\n | \"under_review\"\n | \"planned\"\n | \"in_progress\"\n | \"completed\"\n | \"closed\";\n\n// ============================================\n// Feedback\n// ============================================\n\nexport interface FeedbackItem {\n id: string;\n title: string;\n description: string;\n status: FeedbackStatus;\n voteCount: number;\n commentCount: number;\n isPinned: boolean;\n hasVoted: boolean;\n createdAt: number;\n updatedAt: number;\n completedAt?: number;\n tags: FeedbackTag[];\n boardStatus: {\n id: string;\n name: string;\n color: string;\n } | null;\n author: FeedbackAuthor | null;\n}\n\nexport interface FeedbackDetail extends FeedbackItem {\n isSubscribed: boolean;\n}\n\nexport interface FeedbackTag {\n id: string;\n name: string;\n color: string;\n}\n\nexport interface FeedbackAuthor {\n name?: string;\n email?: string;\n avatar?: string;\n isExternal: boolean;\n}\n\nexport interface FeedbackListResponse {\n items: FeedbackItem[];\n total: number;\n hasMore: boolean;\n}\n\nexport interface FeedbackListParams {\n /** Filter by custom status ID */\n statusId?: string;\n /** Filter by status name */\n status?: FeedbackStatus;\n /** Search in title and description */\n search?: string;\n /** Sort order */\n sortBy?: \"votes\" | \"newest\" | \"oldest\" | \"comments\";\n /** Number of items to return (max 100) */\n limit?: number;\n /** Offset for pagination */\n offset?: number;\n}\n\nexport interface CreateFeedbackParams {\n title: string;\n description: string;\n}\n\nexport interface CreateFeedbackResponse {\n feedbackId: string;\n isApproved: boolean;\n}\n\nexport interface VoteResponse {\n voted: boolean;\n voteCount: number;\n}\n\n// ============================================\n// Comments\n// ============================================\n\nexport interface Comment {\n id: string;\n body: string;\n isOfficial: boolean;\n author: FeedbackAuthor | null;\n replies: CommentReply[];\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface CommentReply {\n id: string;\n body: string;\n isOfficial: boolean;\n author: FeedbackAuthor | null;\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface AddCommentParams {\n feedbackId: string;\n body: string;\n parentId?: string;\n}\n\nexport interface AddCommentResponse {\n commentId: string;\n}\n\n// ============================================\n// Subscriptions\n// ============================================\n\nexport interface SubscribeResponse {\n subscribed: boolean;\n alreadySubscribed?: boolean;\n}\n\nexport interface UnsubscribeResponse {\n unsubscribed: boolean;\n wasNotSubscribed?: boolean;\n}\n\n// ============================================\n// Roadmap\n// ============================================\n\nexport interface Roadmap {\n lanes: RoadmapLane[];\n}\n\nexport interface RoadmapLane {\n id: string;\n name: string;\n color: string;\n items: RoadmapItem[];\n}\n\nexport interface RoadmapItem {\n id: string;\n title: string;\n status: FeedbackStatus;\n voteCount: number;\n}\n\n// ============================================\n// Changelog\n// ============================================\n\nexport interface ChangelogEntry {\n id: string;\n title: string;\n description?: string;\n version?: string;\n publishedAt?: number;\n feedback: { id: string; title: string }[];\n}\n\n// ============================================\n// Errors\n// ============================================\n\nexport class RefletError extends Error {\n readonly status: number;\n readonly code?: string;\n\n constructor(message: string, status: number, code?: string) {\n super(message);\n this.name = \"RefletError\";\n this.status = status;\n this.code = code;\n }\n}\n\nexport class RefletAuthError extends RefletError {\n constructor(message: string) {\n super(message, 401, \"AUTH_ERROR\");\n this.name = \"RefletAuthError\";\n }\n}\n\nexport class RefletValidationError extends RefletError {\n constructor(message: string) {\n super(message, 400, \"VALIDATION_ERROR\");\n this.name = \"RefletValidationError\";\n }\n}\n\nexport class RefletNotFoundError extends RefletError {\n constructor(message: string) {\n super(message, 404, \"NOT_FOUND\");\n this.name = \"RefletNotFoundError\";\n }\n}\n","import {\n type AddCommentParams,\n type AddCommentResponse,\n type BoardConfig,\n type ChangelogEntry,\n type Comment,\n type CreateFeedbackParams,\n type CreateFeedbackResponse,\n type FeedbackDetail,\n type FeedbackListParams,\n type FeedbackListResponse,\n RefletAuthError,\n type RefletConfig,\n RefletError,\n RefletNotFoundError,\n type RefletUser,\n type Roadmap,\n type SubscribeResponse,\n type UnsubscribeResponse,\n type VoteResponse,\n} from \"./types\";\n\nconst DEFAULT_BASE_URL = \"https://grateful-butterfly-1.convex.cloud\";\n\n/**\n * Reflet SDK Client\n *\n * @example\n * ```ts\n * import { Reflet } from '@reflet/sdk';\n *\n * const reflet = new Reflet({\n * publicKey: 'fb_pub_xxx',\n * user: { id: 'user_123', email: 'user@example.com', name: 'John' }\n * });\n *\n * // List feedback\n * const { items } = await reflet.list({ status: 'open' });\n *\n * // Vote on feedback\n * await reflet.vote('feedback_id');\n *\n * // Submit new feedback\n * await reflet.create({ title: 'Great idea', description: 'Details...' });\n * ```\n */\nexport class Reflet {\n private readonly publicKey: string;\n private readonly baseUrl: string;\n private userToken: string | undefined;\n private user: RefletUser | undefined;\n\n constructor(config: RefletConfig) {\n this.publicKey = config.publicKey;\n this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;\n this.userToken = config.userToken;\n this.user = config.user;\n\n if (!this.publicKey) {\n throw new Error(\"Reflet: publicKey is required\");\n }\n }\n\n /**\n * Update the user identification\n */\n setUser(user: RefletUser | undefined): void {\n this.user = user;\n this.userToken = undefined;\n }\n\n /**\n * Set the user token directly (for server-signed tokens)\n */\n setUserToken(token: string | undefined): void {\n this.userToken = token;\n this.user = undefined;\n }\n\n /**\n * Get the board configuration\n */\n async getConfig(): Promise<BoardConfig> {\n return await this.request<BoardConfig>(\"GET\", \"/api/v1/feedback\");\n }\n\n /**\n * List feedback items with optional filtering\n */\n async list(params?: FeedbackListParams): Promise<FeedbackListResponse> {\n const searchParams = new URLSearchParams();\n\n if (params?.statusId) {\n searchParams.set(\"statusId\", params.statusId);\n }\n if (params?.status) {\n searchParams.set(\"status\", params.status);\n }\n if (params?.search) {\n searchParams.set(\"search\", params.search);\n }\n if (params?.sortBy) {\n searchParams.set(\"sortBy\", params.sortBy);\n }\n if (params?.limit) {\n searchParams.set(\"limit\", String(params.limit));\n }\n if (params?.offset) {\n searchParams.set(\"offset\", String(params.offset));\n }\n\n const query = searchParams.toString();\n const url = `/api/v1/feedback/list${query ? `?${query}` : \"\"}`;\n\n return await this.request<FeedbackListResponse>(\"GET\", url);\n }\n\n /**\n * Get a single feedback item by ID\n */\n async get(feedbackId: string): Promise<FeedbackDetail> {\n const result = await this.request<FeedbackDetail | null>(\n \"GET\",\n `/api/v1/feedback/item?id=${encodeURIComponent(feedbackId)}`\n );\n\n if (!result) {\n throw new RefletNotFoundError(\"Feedback not found\");\n }\n\n return result;\n }\n\n /**\n * Create new feedback\n * Requires user identification\n */\n async create(params: CreateFeedbackParams): Promise<CreateFeedbackResponse> {\n return await this.request<CreateFeedbackResponse>(\n \"POST\",\n \"/api/v1/feedback/create\",\n params\n );\n }\n\n /**\n * Toggle vote on feedback\n * Requires user identification\n */\n async vote(\n feedbackId: string,\n type: \"upvote\" | \"downvote\" = \"upvote\"\n ): Promise<VoteResponse> {\n return await this.request<VoteResponse>(\"POST\", \"/api/v1/feedback/vote\", {\n feedbackId,\n voteType: type,\n });\n }\n\n /**\n * Get comments for a feedback item\n */\n async getComments(\n feedbackId: string,\n sortBy: \"newest\" | \"oldest\" = \"oldest\"\n ): Promise<Comment[]> {\n return await this.request<Comment[]>(\n \"GET\",\n `/api/v1/feedback/comments?feedbackId=${encodeURIComponent(feedbackId)}&sortBy=${sortBy}`\n );\n }\n\n /**\n * Add a comment to feedback\n * Requires user identification\n */\n async comment(params: AddCommentParams): Promise<AddCommentResponse> {\n return await this.request<AddCommentResponse>(\n \"POST\",\n \"/api/v1/feedback/comment\",\n params\n );\n }\n\n /**\n * Subscribe to feedback updates\n * Requires user identification\n */\n async subscribe(feedbackId: string): Promise<SubscribeResponse> {\n return await this.request<SubscribeResponse>(\n \"POST\",\n \"/api/v1/feedback/subscribe\",\n {\n feedbackId,\n }\n );\n }\n\n /**\n * Unsubscribe from feedback updates\n * Requires user identification\n */\n async unsubscribe(feedbackId: string): Promise<UnsubscribeResponse> {\n return await this.request<UnsubscribeResponse>(\n \"POST\",\n \"/api/v1/feedback/unsubscribe\",\n {\n feedbackId,\n }\n );\n }\n\n /**\n * Get roadmap data\n */\n async getRoadmap(): Promise<Roadmap> {\n return await this.request<Roadmap>(\"GET\", \"/api/v1/feedback/roadmap\");\n }\n\n /**\n * Get changelog entries\n */\n async getChangelog(limit?: number): Promise<ChangelogEntry[]> {\n const url = limit\n ? `/api/v1/feedback/changelog?limit=${limit}`\n : \"/api/v1/feedback/changelog\";\n\n return await this.request<ChangelogEntry[]>(\"GET\", url);\n }\n\n /**\n * Make an authenticated API request\n */\n private async request<T>(\n method: string,\n path: string,\n body?: unknown\n ): Promise<T> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.publicKey}`,\n };\n\n // Add user token if available\n const token = this.userToken ?? this.generateUserToken();\n if (token) {\n headers[\"X-User-Token\"] = token;\n }\n\n const response = await fetch(`${this.baseUrl}${path}`, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n const data = (await response.json()) as T | { error: string };\n\n if (!response.ok) {\n const errorMessage =\n (data as { error?: string }).error ?? \"Request failed\";\n\n if (response.status === 401) {\n throw new RefletAuthError(errorMessage);\n }\n if (response.status === 404) {\n throw new RefletNotFoundError(errorMessage);\n }\n\n throw new RefletError(errorMessage, response.status);\n }\n\n return data as T;\n }\n\n /**\n * Generate a simple user token for client-side use\n * Note: For production, use server-side token signing\n */\n private generateUserToken(): string | undefined {\n if (!this.user) {\n return undefined;\n }\n\n // Simple base64 encoding for client-side use\n // In production, tokens should be signed server-side\n const payload = {\n id: this.user.id,\n email: this.user.email,\n name: this.user.name,\n iat: Math.floor(Date.now() / 1000),\n exp: Math.floor(Date.now() / 1000) + 86_400, // 24 hours\n };\n\n const header = btoa(JSON.stringify({ alg: \"none\", typ: \"JWT\" }));\n const payloadB64 = btoa(JSON.stringify(payload));\n\n return `${header}.${payloadB64}.`;\n }\n}\n"]}