reflet-sdk 0.5.1 → 0.6.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +24 -4
  3. package/dist/{client-BWEBWk1k.js → client-DAdWdIFY.js} +17 -13
  4. package/dist/client-DAdWdIFY.js.map +1 -0
  5. package/dist/{client-w4Rkl10y.d.ts → client-DF1fdsSC.d.ts} +3 -1
  6. package/dist/{client-w4Rkl10y.d.ts.map → client-DF1fdsSC.d.ts.map} +1 -1
  7. package/dist/{devtools-layer-EyK7n9w3.js → devtools-layer-D_0UL_Wd.js} +2879 -2717
  8. package/dist/devtools-layer-D_0UL_Wd.js.map +1 -0
  9. package/dist/devtools-next.d.ts +1 -1
  10. package/dist/devtools-next.d.ts.map +1 -1
  11. package/dist/devtools-next.js +1 -1
  12. package/dist/devtools-vite.d.ts +1 -1
  13. package/dist/devtools-vite.d.ts.map +1 -1
  14. package/dist/devtools-vite.js +7 -3
  15. package/dist/devtools-vite.js.map +1 -1
  16. package/dist/feedback.d.ts.map +1 -1
  17. package/dist/feedback.js +51 -34
  18. package/dist/feedback.js.map +1 -1
  19. package/dist/{handler-CHP0m7KF.js → handler-C9tdKcl0.js} +335 -46
  20. package/dist/handler-C9tdKcl0.js.map +1 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/{options-7KtQZJj8.d.ts → options-CIBazDPp.d.ts} +3 -1
  24. package/dist/options-CIBazDPp.d.ts.map +1 -0
  25. package/dist/{picker-DFfC1uIY.js → picker-C7TkZcBE.js} +60 -44
  26. package/dist/picker-C7TkZcBE.js.map +1 -0
  27. package/dist/{protocol-Bn6Bg_0r.js → protocol-ClqTFzwR.js} +4 -1
  28. package/dist/protocol-ClqTFzwR.js.map +1 -0
  29. package/dist/react.d.ts +1 -1
  30. package/dist/react.d.ts.map +1 -1
  31. package/dist/react.js +43 -49
  32. package/dist/react.js.map +1 -1
  33. package/dist/{z-index-CKxBgZTG.js → z-index-BL0SGZEP.js} +7 -7
  34. package/dist/z-index-BL0SGZEP.js.map +1 -0
  35. package/package.json +3 -2
  36. package/dist/client-BWEBWk1k.js.map +0 -1
  37. package/dist/devtools-layer-EyK7n9w3.js.map +0 -1
  38. package/dist/handler-CHP0m7KF.js.map +0 -1
  39. package/dist/options-7KtQZJj8.d.ts.map +0 -1
  40. package/dist/picker-DFfC1uIY.js.map +0 -1
  41. package/dist/protocol-Bn6Bg_0r.js.map +0 -1
  42. package/dist/z-index-CKxBgZTG.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ All notable changes to `reflet-sdk` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.0] - 2026-09-30
9
+
10
+ ### Security
11
+
12
+ - The devtools Vite plugin refuses requests from other machines, and the devtools route rejects requests without a same-origin `Sec-Fetch-Site` header unless they come to a plain-HTTP hostname you allowed. Next.js apps must run `next dev -H 127.0.0.1`: see the devtools setup in the README.
13
+ - "Copy as prompt" on board feedback now keeps the page URL and element details inside the untrusted `<user_report>` block, so a reporter cannot write text that looks like instructions from your team.
14
+
15
+ ### Fixed
16
+
17
+ - Failed API responses are checked before their body is parsed, so an error page no longer surfaces as a JSON parse error.
18
+ - Devtools notes release screenshot preview memory when closed.
19
+
20
+ ## [0.5.2] - 2026-09-29
21
+
22
+ ### Added
23
+
24
+ - Connect the devtools Board tab to Reflet without configuring a secret key. Approve access in Reflet and revoke it from the Board tab or Account → Devtools. Credentials stay on the dev server, isolated by project and backend; notes remain internal and are attributed to the connected member.
25
+
26
+ ### Fixed
27
+
28
+ - Using the widget no longer closes the app's open menus, popovers and pickers. Pointing at an element or attaching a screenshot keeps them open, so their content can be reported, and typing in the widget no longer loses focus to a menu's focus trap. The widget also stays clickable while a modal menu disables pointer events on the page.
29
+
8
30
  ## [0.5.1] - 2026-09-28
9
31
 
10
32
  ### Fixed
package/README.md CHANGED
@@ -207,7 +207,15 @@ turn it off.
207
207
  the reporter's source location, and a code search for the element's text.
208
208
 
209
209
  Code view and the board go through one dev-only route that reads files from
210
- your repository and holds the secret key server-side.
210
+ your repository and holds the board credential server-side.
211
+
212
+ To reach the board, click **Connect to Reflet** in the Board tab: sign in on
213
+ reflet.app, approve the organization that owns the widget's key, and the dev
214
+ server keeps a revocable token in `~/.reflet/devtools/`,
215
+ outside the repository. That token acts as you, only reaches the devtools
216
+ endpoints, and expires after 30 days unused; revoke it with Disconnect or from
217
+ Account → Devtools on reflet.app. Connect works on `localhost`, `*.localhost`
218
+ and `*.test`; elsewhere, set `REFLET_SECRET_KEY`.
211
219
 
212
220
  Next.js App Router, `app/api/reflet-devtools/[...path]/route.ts`:
213
221
 
@@ -216,7 +224,16 @@ export { GET, POST } from 'reflet-sdk/devtools/next';
216
224
  ```
217
225
 
218
226
  The handlers answer 404 unless `NODE_ENV` is `development`, so the file is
219
- safe to ship. Vite:
227
+ safe to ship. `next dev` listens on every network interface by default, and a
228
+ route handler cannot see who is calling, so bind it to loopback — otherwise
229
+ anyone on your Wi-Fi can read your source through the route:
230
+
231
+ ```json
232
+ "dev": "next dev -H 127.0.0.1"
233
+ ```
234
+
235
+ Vite listens on localhost unless you pass `--host`, and the plugin refuses
236
+ requests that do not come from this machine either way. Vite:
220
237
 
221
238
  ```ts
222
239
  import { refletDevtools } from 'reflet-sdk/devtools/vite';
@@ -226,14 +243,17 @@ export default defineConfig({ plugins: [react(), refletDevtools()] });
226
243
 
227
244
  | Env var | Purpose |
228
245
  |---------|---------|
229
- | `REFLET_SECRET_KEY` | Your `fb_sec_…` key, server-only. Needed to send notes and read the board. |
246
+ | `REFLET_SECRET_KEY` | Your `fb_sec_…` key, server-only. Optional: replaces Connect, and wins over it when set. |
230
247
  | `REFLET_EDITOR` | `vscode` (default), `cursor`, `windsurf`, `zed` or `webstorm` for "Open in editor". |
231
248
  | `REFLET_API_URL` | API override, for self-hosted or local Reflet backends. |
249
+ | `REFLET_APP_URL` | Web app origin Connect signs in on. Defaults to `https://www.reflet.app`. Same as the `appUrl` option. |
232
250
  | `REFLET_DEVTOOLS_HOSTS` | Comma-separated dev hostnames besides localhost, e.g. `app.test`. Same as the `allowedHosts` option. |
233
251
 
234
252
  The route only answers on `localhost`, `*.localhost`, loopback IPs and the
235
253
  hostnames you allow — so a rebound DNS name pointing at your machine gets a 403
236
- — and only to same-origin requests carrying the devtools header. It only serves
254
+ — and only to same-origin browser requests (`Sec-Fetch-Site`) carrying the
255
+ devtools header. These checks stop other sites in your browser, not other
256
+ machines: that is what the loopback binding is for. It only serves
237
257
  source files (`.ts`, `.tsx`, `.js`, `.jsx`, `.vue`, `.svelte`, `.astro`,
238
258
  `.mdx`…) inside the repository and never under `node_modules`, and only
239
259
  forwards the feedback endpoints the devtools use.
@@ -30,6 +30,7 @@ var RefletNotFoundError = class extends RefletError {
30
30
  //#endregion
31
31
  //#region src/client.ts
32
32
  const DEFAULT_API_URL = "https://harmless-clam-802.convex.site";
33
+ const UNSIGNED_TOKEN_ALGORITHM = "none";
33
34
  /** btoa is Latin1-only — a name like "José" would throw. */
34
35
  function base64Utf8(value) {
35
36
  let binary = "";
@@ -223,9 +224,15 @@ var Reflet = class {
223
224
  * Make an authenticated API request
224
225
  */
225
226
  async request(method, path, body) {
226
- let response;
227
+ const response = await this.send(method, path, body);
228
+ if (!response.ok) return this.throwResponseError(response);
229
+ const text = await response.text();
230
+ if (!text) return {};
231
+ return this.parseJsonSafely(text, response.status);
232
+ }
233
+ async send(method, path, body) {
227
234
  try {
228
- response = await fetch(`${this.baseUrl}${path}`, {
235
+ return await fetch(`${this.baseUrl}${path}`, {
229
236
  body: body ? JSON.stringify(body) : void 0,
230
237
  headers: this.buildHeaders(),
231
238
  method
@@ -233,17 +240,14 @@ var Reflet = class {
233
240
  } catch (networkError) {
234
241
  throw new RefletError(`Network error: ${networkError instanceof Error ? networkError.message : "Failed to connect"}`, 0);
235
242
  }
243
+ }
244
+ async throwResponseError(response) {
245
+ const fallbackMessage = `Request failed with status ${response.status}`;
236
246
  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
- }
247
+ if (!text) this.throwHttpError(fallbackMessage, response.status);
241
248
  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;
249
+ const errorMessage = this.isErrorResponse(data) ? data.error : fallbackMessage;
250
+ this.throwHttpError(errorMessage, response.status);
247
251
  }
248
252
  /**
249
253
  * Unsigned identity from the `user` option. The API attributes reports with
@@ -259,7 +263,7 @@ var Reflet = class {
259
263
  name: this.user.name
260
264
  };
261
265
  return `${base64Utf8(JSON.stringify({
262
- alg: "none",
266
+ alg: UNSIGNED_TOKEN_ALGORITHM,
263
267
  typ: "JWT"
264
268
  }))}.${base64Utf8(JSON.stringify(payload))}.`;
265
269
  }
@@ -267,4 +271,4 @@ var Reflet = class {
267
271
  //#endregion
268
272
  export { RefletNotFoundError as a, RefletError as i, Reflet as n, RefletValidationError as o, RefletAuthError as r, DEFAULT_API_URL as t };
269
273
 
270
- //# sourceMappingURL=client-BWEBWk1k.js.map
274
+ //# sourceMappingURL=client-DAdWdIFY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-DAdWdIFY.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 /** What the reporter wrote about this element while picking it. */\n comment?: string;\n /** React component names owning the element, innermost first. */\n componentStack: string[];\n /** Truncated, redacted 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 /** Page area holding the element, e.g. `dialog \"Members\" › Danger zone`. */\n region?: string;\n selector: string;\n /** `src/components/login-form.tsx:46:19` when the React build exposes it. */\n sourceLocation?: string;\n /** Collapsed visible text of the element — survives markup truncation. */\n text?: 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 /** Where the viewport sat in the page when the screenshot was taken. */\n scroll?: { x: number; y: 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 selections?: 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:\n | \"arrow\"\n | \"blur\"\n | \"highlight\"\n | \"pen\"\n | \"rectangle\"\n | \"spotlight\"\n | \"text\";\n width?: number;\n x: number;\n y: number;\n}\n\nexport interface SaveScreenshotParams {\n annotatedStorageId?: string;\n annotations?: ScreenshotAnnotation[];\n /** `element` for the close-up of a picked element, `widget` for the page. */\n captureSource?: \"element\" | \"widget\";\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\nexport const DEFAULT_API_URL = \"https://harmless-clam-802.convex.site\";\nconst UNSIGNED_TOKEN_ALGORITHM = \"none\";\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 const response = await this.send(method, path, body);\n\n if (!response.ok) {\n return this.throwResponseError(response);\n }\n\n const text = await response.text();\n\n // Successful empty body (e.g. 204 No Content) — return empty object\n // narrowed through unknown for type-safe assertion\n if (!text) {\n const emptyResponse: unknown = {};\n return emptyResponse as T;\n }\n\n // Standard unknown → T assertion for runtime-parsed JSON\n return this.parseJsonSafely(text, response.status) as T;\n }\n\n private async send(\n method: string,\n path: string,\n body?: unknown\n ): Promise<Response> {\n try {\n return 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\n private async throwResponseError(response: Response): Promise<never> {\n const fallbackMessage = `Request failed with status ${response.status}`;\n const text = await response.text();\n if (!text) {\n this.throwHttpError(fallbackMessage, response.status);\n }\n\n const data = this.parseJsonSafely(text, response.status);\n const errorMessage = this.isErrorResponse(data)\n ? data.error\n : fallbackMessage;\n this.throwHttpError(errorMessage, response.status);\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(\n JSON.stringify({ alg: UNSIGNED_TOKEN_ALGORITHM, typ: \"JWT\" })\n );\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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"}
@@ -118,6 +118,8 @@ declare class Reflet {
118
118
  * Make an authenticated API request
119
119
  */
120
120
  private request;
121
+ private send;
122
+ private throwResponseError;
121
123
  /**
122
124
  * Unsigned identity from the `user` option. The API attributes reports with
123
125
  * it but refuses voting, commenting and subscribing — those need `signUser`.
@@ -126,4 +128,4 @@ declare class Reflet {
126
128
  }
127
129
  //#endregion
128
130
  export { Reflet as t };
129
- //# sourceMappingURL=client-w4Rkl10y.d.ts.map
131
+ //# sourceMappingURL=client-DF1fdsSC.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client-w4Rkl10y.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cAwDa;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"}
1
+ {"version":3,"file":"client-DF1fdsSC.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;UAwBA;UAoBA;;;;;UAkBN"}