reflet-sdk 0.4.2 → 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 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.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
+
23
+ ## [0.4.3] - 2026-09-22
24
+
25
+ ### Fixed
26
+
27
+ - Host shortcuts that listen in the capture phase no longer see keys typed in the widget either — the widget stops them before the host and replays them inside its own shadow root
28
+ - Only a host capture listener registered before `reflet-sdk/feedback` is imported still runs first
29
+
8
30
  ## [0.4.2] - 2026-09-22
9
31
 
10
32
  ### 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 ?? DEFAULT_API_URL;
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 { RefletValidationError as a, RefletNotFoundError as i, RefletAuthError as n, RefletError as r, Reflet as t };
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-BYuSKJw5.js.map
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":";;;;;;;;;;;;;;;;;;;;;;;;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"}
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"}