reflet-sdk 0.4.3 → 0.5.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 +15 -0
- package/README.md +64 -0
- package/dist/{client-BYuSKJw5.js → client-BWEBWk1k.js} +3 -3
- package/dist/client-BWEBWk1k.js.map +1 -0
- package/dist/client-w4Rkl10y.d.ts.map +1 -1
- package/dist/devtools-layer-BYGuFmhg.js +3935 -0
- package/dist/devtools-layer-BYGuFmhg.js.map +1 -0
- package/dist/devtools-next.d.ts +19 -0
- package/dist/devtools-next.d.ts.map +1 -0
- package/dist/devtools-next.js +25 -0
- package/dist/devtools-next.js.map +1 -0
- package/dist/devtools-vite.d.ts +8 -0
- package/dist/devtools-vite.d.ts.map +1 -0
- package/dist/devtools-vite.js +75 -0
- package/dist/devtools-vite.js.map +1 -0
- package/dist/feedback.d.ts +5 -0
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +54 -1868
- package/dist/feedback.js.map +1 -1
- package/dist/handler-CHP0m7KF.js +713 -0
- package/dist/handler-CHP0m7KF.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/options-7KtQZJj8.d.ts +20 -0
- package/dist/options-7KtQZJj8.d.ts.map +1 -0
- package/dist/picker-DDDpVcfr.js +1918 -0
- package/dist/picker-DDDpVcfr.js.map +1 -0
- package/dist/protocol-Bn6Bg_0r.js +32 -0
- package/dist/protocol-Bn6Bg_0r.js.map +1 -0
- package/dist/react.js +2 -2
- package/dist/{z-index-_Q9PWbfY.js → z-index-CKxBgZTG.js} +3 -2
- package/dist/{z-index-_Q9PWbfY.js.map → z-index-CKxBgZTG.js.map} +1 -1
- package/package.json +15 -2
- package/dist/client-BYuSKJw5.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ 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.5.0] - 2026-09-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Devtools for the team, development only: a draggable bar above the launcher with a movable, resizable full-height panel to pick an element and write a note on it (shown instantly as a numbered pin, stored in IndexedDB) or Shift+click it to open its source, copy notes as an agent prompt, send them to the board as internal feedback, and read the board's feedback for the current page
|
|
13
|
+
- `reflet-sdk/devtools/next` and `reflet-sdk/devtools/vite`: the dev-only route behind code view and the board; it reads files inside the repository and keeps `REFLET_SECRET_KEY` on the server
|
|
14
|
+
- "Clear notes" in the devtools archives every note at once; cleared notes can be restored for a day, then are deleted for good
|
|
15
|
+
- `devtools` prop on `RefletFeedback` to turn the bar off
|
|
16
|
+
- Internal feedback: `POST /api/v1/feedback/create` accepts `internal: true` with a secret key, and `GET /api/v1/feedback/list` accepts `pagePath` to list what was reported on one route
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Screenshots no longer wait out the widget's own animations before capturing
|
|
21
|
+
- A moved widget that had to squeeze into a smaller window goes back to where it was put once the window grows again
|
|
22
|
+
|
|
8
23
|
## [0.4.3] - 2026-09-22
|
|
9
24
|
|
|
10
25
|
### Fixed
|
package/README.md
CHANGED
|
@@ -166,6 +166,7 @@ builds do; production builds strip React's debug data).
|
|
|
166
166
|
| `metadata` | `Record<string, string>` | — | Extra fields merged into every report. |
|
|
167
167
|
| `categories` | `("bug" \| "idea" \| "question")[]` | all three | Category chips to show. |
|
|
168
168
|
| `labels` | `Partial<FeedbackWidgetLabels>` | — | Override any string for i18n. |
|
|
169
|
+
| `devtools` | `boolean` | `true` | Dev-only team tools; see below. Never loaded outside `NODE_ENV=development`. |
|
|
169
170
|
| `onSubmit` | `(result: { feedbackId: string }) => void` | — | Fires after a successful send. |
|
|
170
171
|
|
|
171
172
|
The panel renders in a shadow root, so your CSS cannot reach it and its CSS
|
|
@@ -174,6 +175,69 @@ cannot reach your app. Screenshots are rendered from the DOM rather than through
|
|
|
174
175
|
the trade-off is that cross-origin images without CORS headers, iframes and
|
|
175
176
|
canvas content may come out blank.
|
|
176
177
|
|
|
178
|
+
#### Devtools for your team (development only)
|
|
179
|
+
|
|
180
|
+
In `NODE_ENV=development`, `RefletFeedback` also mounts a small devtools bar
|
|
181
|
+
above its launcher; drag it by its grip anywhere, double-click the grip to put
|
|
182
|
+
it back. Its panel opens full height: drag its header to move it (double-click
|
|
183
|
+
to dock it back), drag its inner edge, bottom edge or corner to resize it
|
|
184
|
+
(double-click a handle to reset). Position and size are remembered and refit
|
|
185
|
+
whenever the window changes size. Production builds drop it at bundle time —
|
|
186
|
+
the devtools chunk is never shipped to your users. Pass `devtools={false}` to
|
|
187
|
+
turn it off.
|
|
188
|
+
|
|
189
|
+
- **Pick** — click an element to write a note on it. The note card has a
|
|
190
|
+
"View code" button, and Shift+click (or Shift+Enter) skips the note and opens
|
|
191
|
+
the source file with the JSX element highlighted, ready to jump to in your
|
|
192
|
+
editor. Locations come from React's debug stacks resolved through the dev
|
|
193
|
+
server's source maps, Server Components included.
|
|
194
|
+
- **Notes** — a note appears the moment you press Enter, as a numbered pin on
|
|
195
|
+
its element, Figma-style; click the pin to read, edit or delete it, and find
|
|
196
|
+
the same number on its card in the panel. Its source location and a
|
|
197
|
+
snapshot of the element in its page are filled in right after, even if you
|
|
198
|
+
reload first. Notes live in this browser's IndexedDB until you delete them.
|
|
199
|
+
Every change shows up at once; if saving fails, a notice says so and offers
|
|
200
|
+
to retry.
|
|
201
|
+
- **Copy as prompt** — every note, with its source location, component stack,
|
|
202
|
+
selector and markup, as one Markdown prompt for a coding agent.
|
|
203
|
+
- **Send to board** — notes become internal feedback: members see them with an
|
|
204
|
+
"Internal" badge, the public board never does, and an admin can make one
|
|
205
|
+
public from the dashboard.
|
|
206
|
+
- **Board** — the board's feedback for the current route, with "Show on page",
|
|
207
|
+
the reporter's source location, and a code search for the element's text.
|
|
208
|
+
|
|
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.
|
|
211
|
+
|
|
212
|
+
Next.js App Router, `app/api/reflet-devtools/[...path]/route.ts`:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
export { GET, POST } from 'reflet-sdk/devtools/next';
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The handlers answer 404 unless `NODE_ENV` is `development`, so the file is
|
|
219
|
+
safe to ship. Vite:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { refletDevtools } from 'reflet-sdk/devtools/vite';
|
|
223
|
+
|
|
224
|
+
export default defineConfig({ plugins: [react(), refletDevtools()] });
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
| Env var | Purpose |
|
|
228
|
+
|---------|---------|
|
|
229
|
+
| `REFLET_SECRET_KEY` | Your `fb_sec_…` key, server-only. Needed to send notes and read the board. |
|
|
230
|
+
| `REFLET_EDITOR` | `vscode` (default), `cursor`, `windsurf`, `zed` or `webstorm` for "Open in editor". |
|
|
231
|
+
| `REFLET_API_URL` | API override, for self-hosted or local Reflet backends. |
|
|
232
|
+
| `REFLET_DEVTOOLS_HOSTS` | Comma-separated dev hostnames besides localhost, e.g. `app.test`. Same as the `allowedHosts` option. |
|
|
233
|
+
|
|
234
|
+
The route only answers on `localhost`, `*.localhost`, loopback IPs and the
|
|
235
|
+
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
|
|
237
|
+
source files (`.ts`, `.tsx`, `.js`, `.jsx`, `.vue`, `.svelte`, `.astro`,
|
|
238
|
+
`.mdx`…) inside the repository and never under `node_modules`, and only
|
|
239
|
+
forwards the feedback endpoints the devtools use.
|
|
240
|
+
|
|
177
241
|
### 5. Server-Side User Signing (required for voting and commenting)
|
|
178
242
|
|
|
179
243
|
`user` is a client-asserted identity: the browser can claim to be anyone, so
|
|
@@ -65,7 +65,7 @@ var Reflet = class {
|
|
|
65
65
|
user;
|
|
66
66
|
constructor(config) {
|
|
67
67
|
this.publicKey = config.publicKey;
|
|
68
|
-
this.baseUrl = config.baseUrl ??
|
|
68
|
+
this.baseUrl = config.baseUrl ?? "https://harmless-clam-802.convex.site";
|
|
69
69
|
this.userToken = config.userToken;
|
|
70
70
|
this.user = config.user;
|
|
71
71
|
if (!this.publicKey) throw new Error("Reflet: publicKey is required");
|
|
@@ -265,6 +265,6 @@ var Reflet = class {
|
|
|
265
265
|
}
|
|
266
266
|
};
|
|
267
267
|
//#endregion
|
|
268
|
-
export {
|
|
268
|
+
export { RefletNotFoundError as a, RefletError as i, Reflet as n, RefletValidationError as o, RefletAuthError as r, DEFAULT_API_URL as t };
|
|
269
269
|
|
|
270
|
-
//# sourceMappingURL=client-
|
|
270
|
+
//# sourceMappingURL=client-BWEBWk1k.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-BWEBWk1k.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\";\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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-w4Rkl10y.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;
|
|
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"}
|