reflet-sdk 0.1.5 → 0.2.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/README.md CHANGED
@@ -113,9 +113,73 @@ function FeedbackPage() {
113
113
  }
114
114
  ```
115
115
 
116
- ### 4. Server-Side User Signing (Recommended for Production)
116
+ ### 4. Floating Feedback Widget
117
117
 
118
- For production apps, sign user tokens on your server to prevent client-side spoofing.
118
+ One component adds a floating button to your app. Opening it screenshots the
119
+ current viewport, lets the user draw on that screenshot, and lets them point at
120
+ an element so the report carries the React component behind it.
121
+
122
+ ```bash
123
+ npx reflet-cli init
124
+ ```
125
+
126
+ Or mount it yourself, once, as the last child of your app shell:
127
+
128
+ ```tsx
129
+ import { RefletFeedback } from 'reflet-sdk/feedback';
130
+
131
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
132
+ return (
133
+ <html lang="en">
134
+ <body>
135
+ {children}
136
+ <RefletFeedback publicKey={process.env.NEXT_PUBLIC_REFLET_PUBLIC_KEY} />
137
+ </body>
138
+ </html>
139
+ );
140
+ }
141
+ ```
142
+
143
+ The entry ships its own `"use client"` directive, so a Next.js layout can stay a
144
+ Server Component. With Vite, read the key from
145
+ `import.meta.env.VITE_REFLET_PUBLIC_KEY` and render the widget next to `<App />`.
146
+
147
+ Every report carries the URL, page title, browser, OS, device, viewport, locale
148
+ and timezone, plus the last 30 console errors and warnings. Pointing at an
149
+ element adds a selector that resolves back to it, the React component stack, and
150
+ the source file and line when the build exposes them (development and preview
151
+ builds do; production builds strip React's debug data).
152
+
153
+ | Prop | Type | Default | Description |
154
+ |------|------|---------|-------------|
155
+ | `publicKey` | `string` | — | Your `fb_pub_…` key. Optional inside `RefletProvider`. |
156
+ | `user` | `RefletUser` | — | Who is reporting. Hides the email field. |
157
+ | `position` | `"bottom-right" \| "bottom-left" \| "top-right" \| "top-left"` | `"bottom-right"` | Which corner the button sits in. |
158
+ | `enabled` | `boolean` | `true` | Render the widget. Gate it to staff or beta users. |
159
+ | `dismissForDays` | `number` | — | Let a reporter hide the launcher in this browser for this many days. |
160
+ | `captureOnOpen` | `boolean` | `true` | Screenshot as soon as the panel opens. |
161
+ | `captureConsole` | `boolean` | `true` | Record console errors and warnings. |
162
+ | `hotkey` | `string \| null` | `null` | Shortcut toggling the panel, e.g. `"mod+shift+f"`. |
163
+ | `theme` | `"auto" \| "light" \| "dark"` | `"auto"` | Colour scheme. |
164
+ | `primaryColor` | `string` | — | Any CSS color for the button and accents. |
165
+ | `offset` | `number` | `20` | Pixels between the button and the viewport edge. |
166
+ | `metadata` | `Record<string, string>` | — | Extra fields merged into every report. |
167
+ | `categories` | `("bug" \| "idea" \| "question")[]` | all three | Category chips to show. |
168
+ | `labels` | `Partial<FeedbackWidgetLabels>` | — | Override any string for i18n. |
169
+ | `onSubmit` | `(result: { feedbackId: string }) => void` | — | Fires after a successful send. |
170
+
171
+ The panel renders in a shadow root, so your CSS cannot reach it and its CSS
172
+ cannot reach your app. Screenshots are rendered from the DOM rather than through
173
+ `getDisplayMedia`, so users are never asked for a screen-share permission —
174
+ the trade-off is that cross-origin images without CORS headers, iframes and
175
+ canvas content may come out blank.
176
+
177
+ ### 5. Server-Side User Signing (required for voting and commenting)
178
+
179
+ `user` is a client-asserted identity: the browser can claim to be anyone, so
180
+ the API accepts it only to attribute a report or a survey answer. Voting,
181
+ commenting and subscribing require a token signed on your server with
182
+ `signUser` — anything else is rejected with a 401.
119
183
 
120
184
  **On your server** (works with any backend: Express, Fastify, Hono, serverless functions, etc.):
121
185
 
@@ -277,11 +341,11 @@ const reflet = new Reflet(config: RefletConfig);
277
341
  | Option | Type | Required | Description |
278
342
  |--------|------|----------|-------------|
279
343
  | `publicKey` | `string` | Yes | Your organization's public API key (`fb_pub_...`) |
280
- | `user` | `RefletUser` | No | User information for client-side identification |
281
- | `userToken` | `string` | No | Signed JWT from your server (use instead of `user` for production) |
344
+ | `user` | `RefletUser` | No | Client-asserted identity — attributes reports only, never grants voting or commenting |
345
+ | `userToken` | `string` | No | Signed JWT from your server (`signUser`), required for voting, commenting and subscribing |
282
346
  | `baseUrl` | `string` | No | Custom API endpoint (defaults to Reflet production API) |
283
347
 
284
- > **Note:** Provide either `user` OR `userToken`, not both. If neither is provided, the user can only read feedback (no voting, commenting, or creating).
348
+ > **Note:** Provide either `user` OR `userToken`, not both. With neither, the user can only read feedback and submit anonymous reports. With `user` alone, reports are attributed but voting, commenting and subscribing stay unavailable.
285
349
 
286
350
  #### Methods
287
351
 
@@ -403,7 +467,7 @@ try {
403
467
 
404
468
  | Error | Cause | Solution |
405
469
  |-------|-------|----------|
406
- | `RefletAuthError` | No user/token provided | Pass `user` or `userToken` to the client |
470
+ | `RefletAuthError` | No signed token provided | Pass a `userToken` from `signUser`; `user` alone cannot vote or comment |
407
471
  | `RefletAuthError` | Invalid or expired token | Generate a new token on your server |
408
472
  | `RefletNotFoundError` | Invalid feedback ID | Check the ID exists |
409
473
  | `RefletError` (403) | Organization is private | Use a secret key or make organization public |
@@ -522,7 +586,7 @@ function App() {
522
586
 
523
587
  **Cause:** Trying to vote, comment, or create feedback without user identification.
524
588
 
525
- **Solution:** Provide `user` or `userToken` when initializing the client:
589
+ **Solution:** Provide a server-signed `userToken` when initializing the client:
526
590
 
527
591
  ```typescript
528
592
  const reflet = new Reflet({
@@ -0,0 +1,270 @@
1
+ //#region 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
+ //#endregion
31
+ //#region src/client.ts
32
+ const DEFAULT_API_URL = "https://harmless-clam-802.convex.site";
33
+ /** btoa is Latin1-only — a name like "José" would throw. */
34
+ function base64Utf8(value) {
35
+ let binary = "";
36
+ for (const byte of new TextEncoder().encode(value)) binary += String.fromCharCode(byte);
37
+ return btoa(binary);
38
+ }
39
+ /**
40
+ * Reflet SDK Client
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import { Reflet } from 'reflet-sdk';
45
+ *
46
+ * const reflet = new Reflet({
47
+ * publicKey: 'fb_pub_xxx',
48
+ * user: { id: 'user_123', email: 'user@example.com', name: 'John' }
49
+ * });
50
+ *
51
+ * // List feedback
52
+ * const { items } = await reflet.list({ status: 'open' });
53
+ *
54
+ * // Vote on feedback
55
+ * await reflet.vote('feedback_id');
56
+ *
57
+ * // Submit new feedback
58
+ * await reflet.create({ title: 'Great idea', description: 'Details...' });
59
+ * ```
60
+ */
61
+ var Reflet = class {
62
+ publicKey;
63
+ baseUrl;
64
+ userToken;
65
+ user;
66
+ constructor(config) {
67
+ this.publicKey = config.publicKey;
68
+ this.baseUrl = config.baseUrl ?? DEFAULT_API_URL;
69
+ this.userToken = config.userToken;
70
+ this.user = config.user;
71
+ if (!this.publicKey) throw new Error("Reflet: publicKey is required");
72
+ }
73
+ /**
74
+ * Update the user identification
75
+ */
76
+ setUser(user) {
77
+ this.user = user;
78
+ this.userToken = void 0;
79
+ }
80
+ /**
81
+ * Set the user token directly (for server-signed tokens)
82
+ */
83
+ setUserToken(token) {
84
+ this.userToken = token;
85
+ this.user = void 0;
86
+ }
87
+ /**
88
+ * Get the organization configuration
89
+ */
90
+ async getConfig() {
91
+ return await this.request("GET", "/api/v1/feedback");
92
+ }
93
+ /**
94
+ * List feedback items with optional filtering
95
+ */
96
+ async list(params) {
97
+ const searchParams = new URLSearchParams();
98
+ if (params?.statusId) searchParams.set("statusId", params.statusId);
99
+ if (params?.status) searchParams.set("status", params.status);
100
+ if (params?.search) searchParams.set("search", params.search);
101
+ if (params?.sortBy) searchParams.set("sortBy", params.sortBy);
102
+ if (params?.limit) searchParams.set("limit", String(params.limit));
103
+ if (params?.offset) searchParams.set("offset", String(params.offset));
104
+ const query = searchParams.toString();
105
+ const url = `/api/v1/feedback/list${query ? `?${query}` : ""}`;
106
+ return await this.request("GET", url);
107
+ }
108
+ /**
109
+ * Get a single feedback item by ID
110
+ */
111
+ async get(feedbackId) {
112
+ const result = await this.request("GET", `/api/v1/feedback/item?id=${encodeURIComponent(feedbackId)}`);
113
+ if (!result) throw new RefletNotFoundError("Feedback not found");
114
+ return result;
115
+ }
116
+ /**
117
+ * Create new feedback
118
+ * Works anonymously or with user identification
119
+ */
120
+ async create(params) {
121
+ return await this.request("POST", "/api/v1/feedback/create", params);
122
+ }
123
+ /**
124
+ * Ask the API for a one-shot storage URL to upload a screenshot to.
125
+ */
126
+ async getScreenshotUploadUrl() {
127
+ return await this.request("POST", "/api/v1/feedback/screenshot/upload-url");
128
+ }
129
+ /**
130
+ * Attach an already uploaded screenshot to a feedback item.
131
+ */
132
+ async saveScreenshot(params) {
133
+ return await this.request("POST", "/api/v1/feedback/screenshot/save", params);
134
+ }
135
+ /**
136
+ * Toggle vote on feedback
137
+ * Requires user identification
138
+ */
139
+ async vote(feedbackId, type = "upvote") {
140
+ return await this.request("POST", "/api/v1/feedback/vote", {
141
+ feedbackId,
142
+ voteType: type
143
+ });
144
+ }
145
+ /**
146
+ * Get comments for a feedback item
147
+ */
148
+ async getComments(feedbackId, sortBy = "oldest") {
149
+ return await this.request("GET", `/api/v1/feedback/comments?feedbackId=${encodeURIComponent(feedbackId)}&sortBy=${sortBy}`);
150
+ }
151
+ /**
152
+ * Add a comment to feedback
153
+ * Requires user identification
154
+ */
155
+ async comment(params) {
156
+ return await this.request("POST", "/api/v1/feedback/comment", params);
157
+ }
158
+ /**
159
+ * Subscribe to feedback updates
160
+ * Requires user identification
161
+ */
162
+ async subscribe(feedbackId) {
163
+ return await this.request("POST", "/api/v1/feedback/subscribe", { feedbackId });
164
+ }
165
+ /**
166
+ * Unsubscribe from feedback updates
167
+ * Requires user identification
168
+ */
169
+ async unsubscribe(feedbackId) {
170
+ return await this.request("POST", "/api/v1/feedback/unsubscribe", { feedbackId });
171
+ }
172
+ /**
173
+ * Get roadmap data
174
+ */
175
+ async getRoadmap() {
176
+ return await this.request("GET", "/api/v1/feedback/roadmap");
177
+ }
178
+ /**
179
+ * Get changelog entries
180
+ */
181
+ async getChangelog(limit) {
182
+ const url = limit ? `/api/v1/feedback/changelog?limit=${limit}` : "/api/v1/feedback/changelog";
183
+ return await this.request("GET", url);
184
+ }
185
+ /**
186
+ * Build request headers with authentication
187
+ */
188
+ buildHeaders() {
189
+ const headers = {
190
+ Authorization: `Bearer ${this.publicKey}`,
191
+ "Content-Type": "application/json"
192
+ };
193
+ const token = this.userToken ?? this.generateUserToken();
194
+ if (token) headers["X-User-Token"] = token;
195
+ return headers;
196
+ }
197
+ /**
198
+ * Parse JSON response text safely.
199
+ * Returns `unknown` — callers must narrow before use.
200
+ */
201
+ parseJsonSafely(text, status) {
202
+ try {
203
+ return JSON.parse(text);
204
+ } catch (parseError) {
205
+ throw new RefletError(`Invalid response: ${parseError instanceof Error ? parseError.message : "Failed to parse JSON"}`, status);
206
+ }
207
+ }
208
+ /**
209
+ * Type guard for API error responses
210
+ */
211
+ isErrorResponse(data) {
212
+ return typeof data === "object" && data !== null && "error" in data && typeof data.error === "string";
213
+ }
214
+ /**
215
+ * Throw appropriate error based on status code
216
+ */
217
+ throwHttpError(message, status) {
218
+ if (status === 401) throw new RefletAuthError(message);
219
+ if (status === 404) throw new RefletNotFoundError(message);
220
+ throw new RefletError(message, status);
221
+ }
222
+ /**
223
+ * Make an authenticated API request
224
+ */
225
+ async request(method, path, body) {
226
+ let response;
227
+ try {
228
+ response = await fetch(`${this.baseUrl}${path}`, {
229
+ body: body ? JSON.stringify(body) : void 0,
230
+ headers: this.buildHeaders(),
231
+ method
232
+ });
233
+ } catch (networkError) {
234
+ throw new RefletError(`Network error: ${networkError instanceof Error ? networkError.message : "Failed to connect"}`, 0);
235
+ }
236
+ const text = await response.text();
237
+ if (!text) {
238
+ if (!response.ok) this.throwHttpError(`Request failed with status ${response.status}`, response.status);
239
+ return {};
240
+ }
241
+ const data = this.parseJsonSafely(text, response.status);
242
+ if (!response.ok) {
243
+ const errorMessage = this.isErrorResponse(data) ? data.error : `Request failed with status ${response.status}`;
244
+ this.throwHttpError(errorMessage, response.status);
245
+ }
246
+ return data;
247
+ }
248
+ /**
249
+ * Unsigned identity from the `user` option. The API attributes reports with
250
+ * it but refuses voting, commenting and subscribing — those need `signUser`.
251
+ */
252
+ generateUserToken() {
253
+ if (!this.user) return;
254
+ const payload = {
255
+ email: this.user.email,
256
+ exp: Math.floor(Date.now() / 1e3) + 86400,
257
+ iat: Math.floor(Date.now() / 1e3),
258
+ id: this.user.id,
259
+ name: this.user.name
260
+ };
261
+ return `${base64Utf8(JSON.stringify({
262
+ alg: "none",
263
+ typ: "JWT"
264
+ }))}.${base64Utf8(JSON.stringify(payload))}.`;
265
+ }
266
+ };
267
+ //#endregion
268
+ export { RefletValidationError as a, RefletNotFoundError as i, RefletAuthError as n, RefletError as r, Reflet as t };
269
+
270
+ //# sourceMappingURL=client-BYuSKJw5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-BYuSKJw5.js","names":[],"sources":["../src/types.ts","../src/client.ts"],"sourcesContent":["/**\n * Reflet SDK Types\n */\n\n// ============================================\n// Configuration\n// ============================================\n\nexport interface RefletConfig {\n /** API base URL (optional, defaults to Reflet production API) */\n baseUrl?: string;\n /** Your organization's public API key (fb_pub_xxx) */\n publicKey: 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 /** URL to user's avatar image */\n avatar?: string;\n /** User's email address */\n email?: string;\n /** Unique identifier for the user in your system */\n id: string;\n /** Additional metadata */\n metadata?: Record<string, unknown>;\n /** User's display name */\n name?: string;\n}\n\n// ============================================\n// API Responses\n// ============================================\n\nexport interface OrganizationConfig {\n description?: string;\n id: string;\n isPublic: boolean;\n logo?: string;\n name: string;\n primaryColor?: string;\n settings?: OrganizationSettings;\n slug: string;\n statuses: OrganizationStatus[];\n}\n\nexport interface OrganizationSettings {\n allowAnonymousVoting?: boolean;\n defaultStatus?: FeedbackStatus;\n requireApproval?: boolean;\n}\n\nexport interface OrganizationStatus {\n color: string;\n icon?: string;\n id: string;\n name: 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 author: FeedbackAuthor | null;\n commentCount: number;\n completedAt?: number;\n createdAt: number;\n description: string;\n hasVoted: boolean;\n id: string;\n isPinned: boolean;\n organizationStatus: {\n id: string;\n name: string;\n color: string;\n } | null;\n status: FeedbackStatus;\n tags: FeedbackTag[];\n title: string;\n updatedAt: number;\n voteCount: number;\n}\n\nexport interface FeedbackDetail extends FeedbackItem {\n isSubscribed: boolean;\n}\n\nexport interface FeedbackTag {\n color: string;\n id: string;\n name: string;\n}\n\nexport interface FeedbackAuthor {\n avatar?: string;\n email?: string;\n isExternal: boolean;\n name?: string;\n}\n\nexport interface FeedbackListResponse {\n hasMore: boolean;\n items: FeedbackItem[];\n total: number;\n}\n\nexport interface FeedbackListParams {\n /** Number of items to return (max 100) */\n limit?: number;\n /** Offset for pagination */\n offset?: number;\n /** Search in title and description */\n search?: string;\n /** Sort order */\n sortBy?: \"votes\" | \"newest\" | \"oldest\" | \"comments\";\n /** Filter by status name */\n status?: FeedbackStatus;\n /** Filter by custom status ID */\n statusId?: string;\n}\n\nexport interface ElementRect {\n height: number;\n width: number;\n x: number;\n y: number;\n}\n\nexport interface ElementSelection {\n /** React component names owning the element, innermost first. */\n componentStack: string[];\n /** Truncated outerHTML of the selected element. */\n html: string;\n /** Short human-readable description, e.g. `button \"Sign in\"`. */\n label: string;\n rect: ElementRect;\n selector: string;\n /** `src/components/login-form.tsx:46:19` when the React build exposes it. */\n sourceLocation?: string;\n}\n\nexport interface ConsoleEvent {\n level: \"error\" | \"warn\";\n message: string;\n timestamp: number;\n}\n\nexport interface PageContext {\n browser?: string;\n device?: string;\n language?: string;\n os?: string;\n pageTitle?: string;\n referrer?: string;\n screen?: { height: number; width: number };\n timezone?: string;\n url?: string;\n userAgent?: string;\n viewport?: { devicePixelRatio: number; height: number; width: number };\n}\n\n/** Everything the widget knows about where and how the feedback was written. */\nexport interface FeedbackContext extends PageContext {\n consoleEvents?: ConsoleEvent[];\n metadata?: Record<string, string>;\n sdkVersion?: string;\n selection?: ElementSelection;\n}\n\nexport interface CreateFeedbackParams {\n context?: FeedbackContext;\n description: string;\n title: string;\n}\n\nexport interface ScreenshotAnnotation {\n color?: string;\n endX?: number;\n endY?: number;\n height?: number;\n points?: Array<{ x: number; y: number }>;\n text?: string;\n type: \"arrow\" | \"blur\" | \"highlight\" | \"pen\" | \"rectangle\" | \"text\";\n width?: number;\n x: number;\n y: number;\n}\n\nexport interface SaveScreenshotParams {\n annotatedStorageId?: string;\n annotations?: ScreenshotAnnotation[];\n feedbackId: string;\n filename?: string;\n height?: number;\n mimeType?: string;\n pageUrl?: string;\n size?: number;\n storageId: string;\n width?: number;\n}\n\nexport interface CreateFeedbackResponse {\n feedbackId: string;\n isApproved: boolean;\n}\n\nexport interface VoteResponse {\n voteCount: number;\n voted: boolean;\n}\n\n// ============================================\n// Comments\n// ============================================\n\nexport interface Comment {\n author: FeedbackAuthor | null;\n body: string;\n createdAt: number;\n id: string;\n isOfficial: boolean;\n replies: CommentReply[];\n updatedAt: number;\n}\n\nexport interface CommentReply {\n author: FeedbackAuthor | null;\n body: string;\n createdAt: number;\n id: string;\n isOfficial: boolean;\n updatedAt: number;\n}\n\nexport interface AddCommentParams {\n body: string;\n feedbackId: string;\n parentId?: string;\n}\n\nexport interface AddCommentResponse {\n commentId: string;\n}\n\n// ============================================\n// Subscriptions\n// ============================================\n\nexport interface SubscribeResponse {\n alreadySubscribed?: boolean;\n subscribed: 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 color: string;\n id: string;\n items: RoadmapItem[];\n name: string;\n}\n\nexport interface RoadmapItem {\n id: string;\n status: FeedbackStatus;\n title: string;\n voteCount: number;\n}\n\n// ============================================\n// Changelog\n// ============================================\n\nexport interface ChangelogEntry {\n description?: string;\n feedback: { id: string; title: string }[];\n id: string;\n publishedAt?: number;\n title: string;\n version?: 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 ChangelogEntry,\n type Comment,\n type CreateFeedbackParams,\n type CreateFeedbackResponse,\n type FeedbackDetail,\n type FeedbackListParams,\n type FeedbackListResponse,\n type OrganizationConfig,\n RefletAuthError,\n type RefletConfig,\n RefletError,\n RefletNotFoundError,\n type RefletUser,\n type Roadmap,\n type SaveScreenshotParams,\n type SubscribeResponse,\n type UnsubscribeResponse,\n type VoteResponse,\n} from \"./types\";\n\n// Default to Reflet production API (HTTP routes use .convex.site)\nconst DEFAULT_API_URL = \"https://harmless-clam-802.convex.site\";\n\n/** btoa is Latin1-only — a name like \"José\" would throw. */\nfunction base64Utf8(value: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(value)) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary);\n}\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_API_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 organization configuration\n */\n async getConfig(): Promise<OrganizationConfig> {\n return await this.request<OrganizationConfig>(\"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 * Works anonymously or with 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 * Ask the API for a one-shot storage URL to upload a screenshot to.\n */\n async getScreenshotUploadUrl(): Promise<{ uploadUrl: string }> {\n return await this.request<{ uploadUrl: string }>(\n \"POST\",\n \"/api/v1/feedback/screenshot/upload-url\"\n );\n }\n\n /**\n * Attach an already uploaded screenshot to a feedback item.\n */\n async saveScreenshot(\n params: SaveScreenshotParams\n ): Promise<{ screenshotId: string }> {\n return await this.request<{ screenshotId: string }>(\n \"POST\",\n \"/api/v1/feedback/screenshot/save\",\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 * Build request headers with authentication\n */\n private buildHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.publicKey}`,\n \"Content-Type\": \"application/json\",\n };\n\n const token = this.userToken ?? this.generateUserToken();\n if (token) {\n headers[\"X-User-Token\"] = token;\n }\n\n return headers;\n }\n\n /**\n * Parse JSON response text safely.\n * Returns `unknown` — callers must narrow before use.\n */\n private parseJsonSafely(text: string, status: number): unknown {\n try {\n return JSON.parse(text);\n } catch (parseError) {\n throw new RefletError(\n `Invalid response: ${parseError instanceof Error ? parseError.message : \"Failed to parse JSON\"}`,\n status\n );\n }\n }\n\n /**\n * Type guard for API error responses\n */\n private isErrorResponse(data: unknown): data is { error: string } {\n return (\n typeof data === \"object\" &&\n data !== null &&\n \"error\" in data &&\n typeof (data as Record<string, unknown>).error === \"string\"\n );\n }\n\n /**\n * Throw appropriate error based on status code\n */\n private throwHttpError(message: string, status: number): never {\n if (status === 401) {\n throw new RefletAuthError(message);\n }\n if (status === 404) {\n throw new RefletNotFoundError(message);\n }\n throw new RefletError(message, status);\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 let response: Response;\n try {\n response = await fetch(`${this.baseUrl}${path}`, {\n body: body ? JSON.stringify(body) : undefined,\n headers: this.buildHeaders(),\n method,\n });\n } catch (networkError) {\n const message =\n networkError instanceof Error\n ? networkError.message\n : \"Failed to connect\";\n throw new RefletError(`Network error: ${message}`, 0);\n }\n\n const text = await response.text();\n\n // Handle empty response\n if (!text) {\n if (!response.ok) {\n this.throwHttpError(\n `Request failed with status ${response.status}`,\n response.status\n );\n }\n // Successful empty body (e.g. 204 No Content) — return empty object\n // narrowed through unknown for type-safe assertion\n const emptyResponse: unknown = {};\n return emptyResponse as T;\n }\n\n const data = this.parseJsonSafely(text, response.status);\n\n if (!response.ok) {\n const errorMessage = this.isErrorResponse(data)\n ? data.error\n : `Request failed with status ${response.status}`;\n this.throwHttpError(errorMessage, response.status);\n }\n\n // Standard unknown → T assertion for runtime-parsed JSON\n return data as T;\n }\n\n /**\n * Unsigned identity from the `user` option. The API attributes reports with\n * it but refuses voting, commenting and subscribing — those need `signUser`.\n */\n private generateUserToken(): string | undefined {\n if (!this.user) {\n return;\n }\n\n const payload = {\n email: this.user.email,\n exp: Math.floor(Date.now() / 1000) + 86_400, // 24 hours\n iat: Math.floor(Date.now() / 1000),\n id: this.user.id,\n name: this.user.name,\n };\n\n const header = base64Utf8(JSON.stringify({ alg: \"none\", typ: \"JWT\" }));\n const payloadB64 = base64Utf8(JSON.stringify(payload));\n\n // Empty signature — the server treats this identity as unverified\n return `${header}.${payloadB64}.`;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ import { A as SubscribeResponse, E as Roadmap, M as VoteResponse, _ as OrganizationConfig, d as FeedbackDetail, i as Comment, j as UnsubscribeResponse, k as SaveScreenshotParams, m as FeedbackListResponse, n as AddCommentResponse, o as CreateFeedbackParams, p as FeedbackListParams, r as ChangelogEntry, s as CreateFeedbackResponse, t as AddCommentParams, w as RefletUser, x as RefletConfig } from "./types-xyOadyfX.js";
2
+ //#region src/client.d.ts
3
+ /**
4
+ * Reflet SDK Client
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { Reflet } from 'reflet-sdk';
9
+ *
10
+ * const reflet = new Reflet({
11
+ * publicKey: 'fb_pub_xxx',
12
+ * user: { id: 'user_123', email: 'user@example.com', name: 'John' }
13
+ * });
14
+ *
15
+ * // List feedback
16
+ * const { items } = await reflet.list({ status: 'open' });
17
+ *
18
+ * // Vote on feedback
19
+ * await reflet.vote('feedback_id');
20
+ *
21
+ * // Submit new feedback
22
+ * await reflet.create({ title: 'Great idea', description: 'Details...' });
23
+ * ```
24
+ */
25
+ declare class Reflet {
26
+ private readonly publicKey;
27
+ private readonly baseUrl;
28
+ private userToken;
29
+ private user;
30
+ constructor(config: RefletConfig);
31
+ /**
32
+ * Update the user identification
33
+ */
34
+ setUser(user: RefletUser | undefined): void;
35
+ /**
36
+ * Set the user token directly (for server-signed tokens)
37
+ */
38
+ setUserToken(token: string | undefined): void;
39
+ /**
40
+ * Get the organization configuration
41
+ */
42
+ getConfig(): Promise<OrganizationConfig>;
43
+ /**
44
+ * List feedback items with optional filtering
45
+ */
46
+ list(params?: FeedbackListParams): Promise<FeedbackListResponse>;
47
+ /**
48
+ * Get a single feedback item by ID
49
+ */
50
+ get(feedbackId: string): Promise<FeedbackDetail>;
51
+ /**
52
+ * Create new feedback
53
+ * Works anonymously or with user identification
54
+ */
55
+ create(params: CreateFeedbackParams): Promise<CreateFeedbackResponse>;
56
+ /**
57
+ * Ask the API for a one-shot storage URL to upload a screenshot to.
58
+ */
59
+ getScreenshotUploadUrl(): Promise<{
60
+ uploadUrl: string;
61
+ }>;
62
+ /**
63
+ * Attach an already uploaded screenshot to a feedback item.
64
+ */
65
+ saveScreenshot(params: SaveScreenshotParams): Promise<{
66
+ screenshotId: string;
67
+ }>;
68
+ /**
69
+ * Toggle vote on feedback
70
+ * Requires user identification
71
+ */
72
+ vote(feedbackId: string, type?: "upvote" | "downvote"): Promise<VoteResponse>;
73
+ /**
74
+ * Get comments for a feedback item
75
+ */
76
+ getComments(feedbackId: string, sortBy?: "newest" | "oldest"): Promise<Comment[]>;
77
+ /**
78
+ * Add a comment to feedback
79
+ * Requires user identification
80
+ */
81
+ comment(params: AddCommentParams): Promise<AddCommentResponse>;
82
+ /**
83
+ * Subscribe to feedback updates
84
+ * Requires user identification
85
+ */
86
+ subscribe(feedbackId: string): Promise<SubscribeResponse>;
87
+ /**
88
+ * Unsubscribe from feedback updates
89
+ * Requires user identification
90
+ */
91
+ unsubscribe(feedbackId: string): Promise<UnsubscribeResponse>;
92
+ /**
93
+ * Get roadmap data
94
+ */
95
+ getRoadmap(): Promise<Roadmap>;
96
+ /**
97
+ * Get changelog entries
98
+ */
99
+ getChangelog(limit?: number): Promise<ChangelogEntry[]>;
100
+ /**
101
+ * Build request headers with authentication
102
+ */
103
+ private buildHeaders;
104
+ /**
105
+ * Parse JSON response text safely.
106
+ * Returns `unknown` — callers must narrow before use.
107
+ */
108
+ private parseJsonSafely;
109
+ /**
110
+ * Type guard for API error responses
111
+ */
112
+ private isErrorResponse;
113
+ /**
114
+ * Throw appropriate error based on status code
115
+ */
116
+ private throwHttpError;
117
+ /**
118
+ * Make an authenticated API request
119
+ */
120
+ private request;
121
+ /**
122
+ * Unsigned identity from the `user` option. The API attributes reports with
123
+ * it but refuses voting, commenting and subscribing — those need `signUser`.
124
+ */
125
+ private generateUserToken;
126
+ }
127
+ //#endregion
128
+ export { Reflet as t };
129
+ //# sourceMappingURL=client-Bi-GlSii.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-Bi-GlSii.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cAyDa;mBACM;mBACA;UACT;UACA;EAER,YAAY,QAAQ;;;;EAcpB,QAAQ,MAAM;;;;EAQd,aAAa;;;;EAQP,aAAa,QAAQ;;;;EAOrB,KAAK,SAAS,qBAAqB,QAAQ;;;;EA+B3C,IAAI,qBAAqB,QAAQ;;;;;EAiBjC,OAAO,QAAQ,uBAAuB,QAAQ;;;;EAW9C,0BAA0B;IAAU;;;;;EAUpC,eACJ,QAAQ,uBACP;IAAU;;;;;;EAYP,KACJ,oBACA,+BACC,QAAQ;;;;EAUL,YACJ,oBACA,+BACC,QAAQ;;;;;EAWL,QAAQ,QAAQ,mBAAmB,QAAQ;;;;;EAY3C,UAAU,qBAAqB,QAAQ;;;;;EAcvC,YAAY,qBAAqB,QAAQ;;;;EAazC,cAAc,QAAQ;;;;EAOtB,aAAa,iBAAiB,QAAQ;;;;UAWpC;;;;;UAkBA;;;;UAcA;;;;UAYA;;;;UAaM;;;;;UAqDN"}