reflet-sdk 0.6.5 → 0.6.6

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 (59) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +100 -2
  3. package/dist/{client-DAdWdIFY.js → client-oFFfRxAM.js} +135 -59
  4. package/dist/client-oFFfRxAM.js.map +1 -0
  5. package/dist/client-transport-B7BdVU7J.d.ts +150 -0
  6. package/dist/client-transport-B7BdVU7J.d.ts.map +1 -0
  7. package/dist/{devtools-layer-CEjzYGBB.js → devtools-layer-B91EVgqg.js} +417 -916
  8. package/dist/devtools-layer-B91EVgqg.js.map +1 -0
  9. package/dist/devtools-next.js +1 -1
  10. package/dist/devtools-vite.js +2 -2
  11. package/dist/feedback.d.ts +7 -1
  12. package/dist/feedback.d.ts.map +1 -1
  13. package/dist/feedback.js +105 -57
  14. package/dist/feedback.js.map +1 -1
  15. package/dist/{handler-BSNmdHXv.js → handler-yA_knIf3.js} +4 -4
  16. package/dist/{handler-BSNmdHXv.js.map → handler-yA_knIf3.js.map} +1 -1
  17. package/dist/index-Buum9tIY.d.ts +230 -0
  18. package/dist/index-Buum9tIY.d.ts.map +1 -0
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +1 -1
  21. package/dist/options-CIBazDPp.d.ts.map +1 -1
  22. package/dist/protocol-vJhQHTcB.js +390 -0
  23. package/dist/protocol-vJhQHTcB.js.map +1 -0
  24. package/dist/{react-context-Caq1fodf.js → react-context-Kazo_JCz.js} +3 -14
  25. package/dist/react-context-Kazo_JCz.js.map +1 -0
  26. package/dist/react.d.ts +43 -3
  27. package/dist/react.d.ts.map +1 -1
  28. package/dist/react.js +389 -3
  29. package/dist/react.js.map +1 -1
  30. package/dist/selection-outline-NzAAAZXR.js +51 -0
  31. package/dist/selection-outline-NzAAAZXR.js.map +1 -0
  32. package/dist/server.js +1 -1
  33. package/dist/server.js.map +1 -1
  34. package/dist/sessions-panel-DVyWzZ3C.js +6011 -0
  35. package/dist/sessions-panel-DVyWzZ3C.js.map +1 -0
  36. package/dist/{submit-MvVjNdYc.js → submit-DYGZQVdG.js} +44 -70
  37. package/dist/submit-DYGZQVdG.js.map +1 -0
  38. package/dist/surveys.d.ts +3 -0
  39. package/dist/surveys.js +2 -0
  40. package/dist/team-activation-DCqh71bl.js +24 -0
  41. package/dist/team-activation-DCqh71bl.js.map +1 -0
  42. package/dist/team-layer-F-9BE4hc.js +423 -0
  43. package/dist/team-layer-F-9BE4hc.js.map +1 -0
  44. package/dist/{types-DnaDEN9Y.d.ts → types-B3XiHPqy.d.ts} +4 -9
  45. package/dist/types-B3XiHPqy.d.ts.map +1 -0
  46. package/dist/use-survey-session-LK6sN1Fg.js +1247 -0
  47. package/dist/use-survey-session-LK6sN1Fg.js.map +1 -0
  48. package/dist/z-index-CwokqVq_.js +15 -0
  49. package/dist/z-index-CwokqVq_.js.map +1 -0
  50. package/package.json +6 -2
  51. package/dist/client-DAdWdIFY.js.map +0 -1
  52. package/dist/client-DF1fdsSC.d.ts +0 -131
  53. package/dist/client-DF1fdsSC.d.ts.map +0 -1
  54. package/dist/devtools-layer-CEjzYGBB.js.map +0 -1
  55. package/dist/protocol-ClqTFzwR.js +0 -35
  56. package/dist/protocol-ClqTFzwR.js.map +0 -1
  57. package/dist/react-context-Caq1fodf.js.map +0 -1
  58. package/dist/submit-MvVjNdYc.js.map +0 -1
  59. package/dist/types-DnaDEN9Y.d.ts.map +0 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ 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.6] - 2026-10-12
9
+
10
+ ### Added
11
+
12
+ - Sessions, in the devtools Sessions tab: talk through your app while you use it, and Reflet records your voice, what you point at and mark, and screenshots at the start, at each mark and on each page change. Form fields appear empty in those screenshots, and password fields, card fields and `data-reflet-redact` elements are left out. Pause stops all collection; a reload stops the recording and never turns the microphone back on. Recordings wait in IndexedDB and upload in order, so going offline or reloading loses nothing already saved.
13
+ - After a session, Reflet transcribes it (Whisper through OpenRouter, zero data retention; audio deleted once transcribed) and proposes what you asked for, each with the quotes and screenshots behind it, what you set aside and what stays unknown. Keep, put aside, dismiss, edit, clarify or merge proposals, correct the transcript, then hand the kept ones off: copy the brief for a coding agent, export it as a `.zip` with its screenshots, or create internal feedback.
14
+ - Team tools in production: members of your Reflet organization open a page with `?reflet-team`, sign in with Reflet in a popup, and record sessions on the deployed app. Tokens are bound to the origin and the widget's key, last 7 days and are revocable from the account page. The key must list the origin under Allowed domains. A page shows one set of team tools however many widgets it mounts, and none where the devtools already run. New `teamTools` (default `true`) and `appUrl` props on `RefletFeedback`.
15
+ - In-app surveys. Mount `<RefletSurveys />` inside `RefletProvider` and every survey the visitor is eligible for is armed — page visit (including client-side navigation), time on page, exit intent, custom events via `useRefletSurveys().track(name)`, and feedback sent from the SDK (the floating widget, `FeedbackDialog`, `useCreateFeedback` or `reflet.create`). One survey shows at a time, in a shadow root at the position set in the dashboard, with branching, several endings, Back, a progress bar, keyboard shortcuts (Enter, Escape, digits, Y/N) and light, dark or automatic theme.
16
+ - `useRefletSurveys()` returns `track`, `showSurvey`, `dismissSurvey` and `activeSurveyId`; calls made before surveys load are queued.
17
+ - `useSurveySession(survey)` and `<SurveyCard />` to run and render surveys in your own UI, also exported from the new `reflet-sdk/surveys` entry with `previewTransport` and `SurveySession`.
18
+ - `Reflet` client methods `getEligibleSurveys`, `startSurveyResponse`, `submitSurveyAnswer`, `completeSurveyResponse` and `dismissSurveyResponse`, and the survey types (`PublicSurvey`, `SurveyQuestion`, `AnswerValue`…).
19
+
20
+ ### Fixed
21
+
22
+ - `ChangelogEntry` now matches what the API returns: linked feedback is in `items` (each with `id`, `title` and `status`), not `feedback`. The changelog widget no longer crashes on load and shows versions as published (`v1.2.3`, not `vv1.2.3`). Unset `ChangelogWidget` props fall back to the widget defaults instead of clearing them.
23
+ - In Firefox, keyboard handlers on the widget's and devtools' controls run again, so Enter saves a picker note, and keys typed in the widget no longer reach the host page's shortcuts. Firefox lets every key press the widget re-sent internally escape back out to the page.
24
+
8
25
  ## [0.6.5] - 2026-10-04
9
26
 
10
27
  ### Changed
package/README.md CHANGED
@@ -167,6 +167,8 @@ builds do; production builds strip React's debug data).
167
167
  | `categories` | `("bug" \| "idea" \| "question")[]` | all three | Category chips to show. |
168
168
  | `labels` | `Partial<FeedbackWidgetLabels>` | — | Override any string for i18n. |
169
169
  | `devtools` | `boolean` | `true` | Dev-only team tools; see below. Never loaded outside `NODE_ENV=development`. |
170
+ | `teamTools` | `boolean` | `true` | Lets signed-in members of your Reflet organization record sessions in production; see below. |
171
+ | `appUrl` | `string` | `"https://www.reflet.app"` | Reflet web app origin team tools sign in on. |
170
172
  | `onSubmit` | `(result: { feedbackId: string }) => void` | — | Fires after a successful send. |
171
173
 
172
174
  The panel renders in a shadow root, so your CSS cannot reach it and its CSS
@@ -208,6 +210,13 @@ turn it off.
208
210
  - **Board** — the board's feedback for the current route, or the whole board,
209
211
  with "Show on page", the reporter's source location, and a code search for
210
212
  the element's text.
213
+ - **Sessions** — talk through the app while you use it: Reflet records your
214
+ voice, what you point at and mark, and a screenshot at the start, at each
215
+ mark and on each page change. Finish, and Reflet transcribes the session and
216
+ proposes the changes you asked for. Keep, edit or dismiss each proposal, then
217
+ hand the kept ones off as a brief to copy into a coding agent or export as a
218
+ `.zip` with its screenshots. Sessions are recorded under your own name, so
219
+ they need Connect rather than `REFLET_SECRET_KEY`.
211
220
 
212
221
  Code view and the board go through one dev-only route that reads files from
213
222
  your repository and holds the board credential server-side.
@@ -259,9 +268,91 @@ devtools header. These checks stop other sites in your browser, not other
259
268
  machines: that is what the loopback binding is for. It only serves
260
269
  source files (`.ts`, `.tsx`, `.js`, `.jsx`, `.vue`, `.svelte`, `.astro`,
261
270
  `.mdx`…) inside the repository and never under `node_modules`, and only
262
- forwards the feedback endpoints the devtools use.
271
+ forwards the feedback and session endpoints the devtools use.
263
272
 
264
- ### 5. Server-Side User Signing (required for voting and commenting)
273
+ #### Sessions in production (team tools)
274
+
275
+ Members of your Reflet organization can record sessions on your deployed app
276
+ too; your users never see it. Open any page with `?reflet-team` once, click
277
+ **Sign in with Reflet**, and approve the app in the popup. The browser keeps a
278
+ token for that origin for 7 days; revoke it from Account → Team tools in your
279
+ apps on reflet.app.
280
+
281
+ Two things on your side:
282
+
283
+ - The widget's public key must list the exact origin under Allowed domains
284
+ (API keys on reflet.app). A key without allowed domains never signs anyone in.
285
+ - The page must allow the microphone. A `Permissions-Policy` header with
286
+ `microphone=()` blocks it; use `microphone=(self)`.
287
+
288
+ Pass `teamTools={false}` to turn it off. Voice is transcribed by Whisper through
289
+ OpenRouter with zero data retention, and the audio is deleted once its
290
+ transcript is saved, within 24 hours at most. Pausing stops all collection;
291
+ keystrokes are never recorded, form fields appear empty in screenshots, and
292
+ password fields and elements marked `data-reflet-redact` are hidden from them.
293
+
294
+ ### 5. In-App Surveys
295
+
296
+ Create surveys in the Reflet dashboard (NPS, ratings, choices, text, statements,
297
+ branching and several endings), then mount `<RefletSurveys />` once inside the
298
+ provider. It loads the surveys this visitor may see — schedule, response cap,
299
+ display frequency and sampling are applied by the API — arms their triggers
300
+ (page visit, time on page, exit intent, custom event, feedback sent through the
301
+ SDK) and shows one at a time in its own shadow root, so your CSS never touches it.
302
+
303
+ ```tsx
304
+ import { RefletProvider, RefletSurveys, useRefletSurveys } from 'reflet-sdk/react';
305
+
306
+ function App() {
307
+ return (
308
+ <RefletProvider publicKey="fb_pub_xxx" user={currentUser}>
309
+ <YourApp />
310
+ <RefletSurveys
311
+ theme="auto"
312
+ onSurveyComplete={({ surveyId, endingId }) => analytics.track('survey_completed', { surveyId, endingId })}
313
+ />
314
+ </RefletProvider>
315
+ );
316
+ }
317
+
318
+ function CheckoutSuccess() {
319
+ const { track, showSurvey } = useRefletSurveys();
320
+ // Fires every survey whose trigger is the custom event "checkout_completed".
321
+ useEffect(() => track('checkout_completed'), [track]);
322
+ // Manual surveys show only when you ask: showSurvey('survey_id').
323
+ return <p>Thanks for your order!</p>;
324
+ }
325
+ ```
326
+
327
+ `track` and `showSurvey` calls made before `<RefletSurveys />` has loaded are
328
+ queued and replayed. Anonymous visitors get a stable id in `localStorage`, so
329
+ "show once" holds across visits; identified users (`user` / `userToken`) are
330
+ matched across devices.
331
+
332
+ | `<RefletSurveys />` prop | Description |
333
+ |---|---|
334
+ | `enabled` | Turn delivery off without unmounting (default `true`) |
335
+ | `theme` | `"auto"` (default), `"light"` or `"dark"` |
336
+ | `primaryColor` | Accent color; defaults to your organization's brand color |
337
+ | `onSurveyStart` / `onSurveyAnswer` / `onSurveyComplete` / `onSurveyDismiss` | Lifecycle callbacks |
338
+
339
+ **Your own UI.** `useSurveySession(survey)` runs the branching flow headlessly
340
+ (`{ snapshot, setAnswer, next, back, dismiss, start }`), and `<SurveyCard
341
+ session={session} variant="floating" | "inline" | "page" />` renders any
342
+ session with its own scoped styles — in a page or a shadow root. Both are also
343
+ available without the provider from `reflet-sdk/surveys`, with
344
+ `previewTransport` to try a survey without saving answers:
345
+
346
+ ```tsx
347
+ import { previewTransport, SurveyCard, useSurveySession } from 'reflet-sdk/surveys';
348
+
349
+ function Preview({ survey }) {
350
+ const session = useSurveySession(survey, { transport: previewTransport });
351
+ return <SurveyCard session={session} variant="inline" />;
352
+ }
353
+ ```
354
+
355
+ ### 6. Server-Side User Signing (required for voting and commenting)
265
356
 
266
357
  `user` is a client-asserted identity: the browser can claim to be anyone, so
267
358
  the API accepts it only to attribute a report or a survey answer. Voting,
@@ -449,6 +540,11 @@ const reflet = new Reflet(config: RefletConfig);
449
540
  | `getComments(feedbackId)` | Get comments for a feedback | No |
450
541
  | `subscribe(feedbackId)` | Subscribe to updates | **Yes** |
451
542
  | `unsubscribe(feedbackId)` | Unsubscribe from updates | **Yes** |
543
+ | `getEligibleSurveys()` | Surveys this visitor may see right now | No |
544
+ | `startSurveyResponse({ surveyId, pageUrl? })` | Open a survey response → `{ responseId }` | No |
545
+ | `submitSurveyAnswer({ responseId, questionId, value })` | Save an answer (`value: null` clears it) | No |
546
+ | `completeSurveyResponse(responseId)` | Finish a response → `{ endingId }` | No |
547
+ | `dismissSurveyResponse(responseId)` | Mark a response abandoned | No |
452
548
 
453
549
  ### React Hooks
454
550
 
@@ -466,6 +562,8 @@ All hooks must be used inside a `<RefletProvider>`.
466
562
  | `useCreateFeedback()` | Create feedback mutation hook | **Yes** |
467
563
  | `useAddComment()` | Add comment mutation hook | **Yes** |
468
564
  | `useSubscription()` | Subscribe/unsubscribe hooks | **Yes** |
565
+ | `useRefletSurveys()` | `track`, `showSurvey`, `dismissSurvey`, `activeSurveyId` for `<RefletSurveys />` | No |
566
+ | `useSurveySession(survey, options?)` | Headless survey runner for custom UIs | No |
469
567
 
470
568
  ### Server Utilities
471
569
 
@@ -28,9 +28,92 @@ var RefletNotFoundError = class extends RefletError {
28
28
  }
29
29
  };
30
30
  //#endregion
31
- //#region src/client.ts
32
- const DEFAULT_API_URL = "https://harmless-clam-802.convex.site";
31
+ //#region src/http-response.ts
32
+ /** Returns `unknown`: callers narrow before use. */
33
+ function parseJsonSafely(text, status) {
34
+ try {
35
+ return JSON.parse(text);
36
+ } catch (parseError) {
37
+ throw new RefletError(`Invalid response: ${parseError instanceof Error ? parseError.message : "Failed to parse JSON"}`, status);
38
+ }
39
+ }
40
+ function isErrorResponse(data) {
41
+ return typeof data === "object" && data !== null && "error" in data && typeof data.error === "string";
42
+ }
43
+ function throwHttpError(message, status) {
44
+ if (status === 401) throw new RefletAuthError(message);
45
+ if (status === 404) throw new RefletNotFoundError(message);
46
+ throw new RefletError(message, status);
47
+ }
48
+ /** Throws the typed Reflet error for a failed response, using the API's `{ error }` message when present. */
49
+ async function throwResponseError(response) {
50
+ const fallbackMessage = `Request failed with status ${response.status}`;
51
+ const text = await response.text();
52
+ if (!text) throwHttpError(fallbackMessage, response.status);
53
+ const data = parseJsonSafely(text, response.status);
54
+ throwHttpError(isErrorResponse(data) ? data.error : fallbackMessage, response.status);
55
+ }
56
+ //#endregion
57
+ //#region src/surveys/registry.ts
58
+ /**
59
+ * One survey controller per project (public key + API URL). Kept apart from
60
+ * the controller so the plain client can signal submitted feedback without
61
+ * bundling the survey runtime.
62
+ */
63
+ const surveyControllers = /* @__PURE__ */ new Map();
64
+ /** `baseUrl` is the resolved API URL, so omitting it and passing the default match. */
65
+ const surveyRegistryKey = (publicKey, baseUrl) => `${baseUrl}|${publicKey}`;
66
+ //#endregion
67
+ //#region ../survey-core/src/respondent.ts
68
+ const RESPONDENT_ID_STORAGE_KEY = "reflet:respondent-id";
69
+ let memoryRespondentId = null;
70
+ /** `crypto.randomUUID` only exists in secure contexts; plain-http intranet sites are not. */
71
+ const newRespondentId = () => Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, "0")).join("");
72
+ /**
73
+ * A stable anonymous id per browser, so display frequency and sampling hold
74
+ * across visits. Falls back to one id per page load when storage is blocked.
75
+ */
76
+ const getRespondentId = () => {
77
+ try {
78
+ const stored = window.localStorage.getItem(RESPONDENT_ID_STORAGE_KEY);
79
+ if (stored) return stored;
80
+ const created = newRespondentId();
81
+ window.localStorage.setItem(RESPONDENT_ID_STORAGE_KEY, created);
82
+ return created;
83
+ } catch {
84
+ memoryRespondentId ??= newRespondentId();
85
+ return memoryRespondentId;
86
+ }
87
+ };
88
+ //#endregion
89
+ //#region src/surveys/survey-api.ts
90
+ /** The anonymous browser id that keeps display frequency and sampling stable. */
91
+ const browserRespondentId = () => typeof window === "undefined" ? void 0 : getRespondentId();
92
+ const createSurveyApi = (request) => ({
93
+ complete: async (responseId) => {
94
+ const { endingId } = await request("POST", "/api/v1/surveys/respond/complete", { responseId });
95
+ return { endingId };
96
+ },
97
+ dismiss: async (responseId) => {
98
+ await request("POST", "/api/v1/surveys/respond/dismiss", { responseId });
99
+ },
100
+ eligible: async () => {
101
+ const respondentId = browserRespondentId();
102
+ const { surveys } = await request("GET", `/api/v1/surveys/eligible${respondentId ? `?respondentId=${encodeURIComponent(respondentId)}` : ""}`);
103
+ return surveys ?? [];
104
+ },
105
+ start: ({ pageUrl, surveyId }) => request("POST", "/api/v1/surveys/respond/start", {
106
+ pageUrl: pageUrl ?? (typeof window === "undefined" ? void 0 : window.location.href),
107
+ respondentId: browserRespondentId(),
108
+ surveyId,
109
+ userAgent: typeof navigator === "undefined" ? void 0 : navigator.userAgent
110
+ }),
111
+ submitAnswer: (params) => request("POST", "/api/v1/surveys/respond/answer", params)
112
+ });
113
+ //#endregion
114
+ //#region src/user-token.ts
33
115
  const UNSIGNED_TOKEN_ALGORITHM = "none";
116
+ const TOKEN_LIFETIME_SECONDS = 86400;
34
117
  /** btoa is Latin1-only — a name like "José" would throw. */
35
118
  function base64Utf8(value) {
36
119
  let binary = "";
@@ -38,6 +121,27 @@ function base64Utf8(value) {
38
121
  return btoa(binary);
39
122
  }
40
123
  /**
124
+ * Unsigned identity from the `user` option. The API attributes reports with
125
+ * it but refuses voting, commenting and subscribing — those need `signUser`.
126
+ */
127
+ function createUnsignedUserToken(user) {
128
+ const issuedAt = Math.floor(Date.now() / 1e3);
129
+ const payload = {
130
+ email: user.email,
131
+ exp: issuedAt + TOKEN_LIFETIME_SECONDS,
132
+ iat: issuedAt,
133
+ id: user.id,
134
+ name: user.name
135
+ };
136
+ return `${base64Utf8(JSON.stringify({
137
+ alg: UNSIGNED_TOKEN_ALGORITHM,
138
+ typ: "JWT"
139
+ }))}.${base64Utf8(JSON.stringify(payload))}.`;
140
+ }
141
+ //#endregion
142
+ //#region src/client.ts
143
+ const DEFAULT_API_URL = "https://harmless-clam-802.convex.site";
144
+ /**
41
145
  * Reflet SDK Client
42
146
  *
43
147
  * @example
@@ -64,6 +168,7 @@ var Reflet = class {
64
168
  baseUrl;
65
169
  userToken;
66
170
  user;
171
+ surveys = createSurveyApi((method, path, body) => this.request(method, path, body));
67
172
  constructor(config) {
68
173
  this.publicKey = config.publicKey;
69
174
  this.baseUrl = config.baseUrl ?? "https://harmless-clam-802.convex.site";
@@ -119,7 +224,9 @@ var Reflet = class {
119
224
  * Works anonymously or with user identification
120
225
  */
121
226
  async create(params) {
122
- return await this.request("POST", "/api/v1/feedback/create", params);
227
+ const created = await this.request("POST", "/api/v1/feedback/create", params);
228
+ surveyControllers.get(surveyRegistryKey(this.publicKey, this.baseUrl))?.notifyFeedbackSubmitted();
229
+ return created;
123
230
  }
124
231
  /**
125
232
  * Ask the API for a one-shot storage URL to upload a screenshot to.
@@ -183,6 +290,26 @@ var Reflet = class {
183
290
  const url = limit ? `/api/v1/feedback/changelog?limit=${limit}` : "/api/v1/feedback/changelog";
184
291
  return await this.request("GET", url);
185
292
  }
293
+ /** Surveys this visitor may see now; schedule, cap, frequency and sampling are applied by the API. */
294
+ getEligibleSurveys() {
295
+ return this.surveys.eligible();
296
+ }
297
+ /** Opens a response before the first answer is saved. */
298
+ startSurveyResponse(params) {
299
+ return this.surveys.start(params);
300
+ }
301
+ /** Saves one answer; `value: null` clears it. */
302
+ submitSurveyAnswer(params) {
303
+ return this.surveys.submitAnswer(params);
304
+ }
305
+ /** Finishes a response; the API walks the flow and returns the ending reached. */
306
+ completeSurveyResponse(responseId) {
307
+ return this.surveys.complete(responseId);
308
+ }
309
+ /** Marks an unfinished response abandoned. */
310
+ dismissSurveyResponse(responseId) {
311
+ return this.surveys.dismiss(responseId);
312
+ }
186
313
  /**
187
314
  * Build request headers with authentication
188
315
  */
@@ -191,44 +318,19 @@ var Reflet = class {
191
318
  Authorization: `Bearer ${this.publicKey}`,
192
319
  "Content-Type": "application/json"
193
320
  };
194
- const token = this.userToken ?? this.generateUserToken();
321
+ const token = this.userToken ?? (this.user && createUnsignedUserToken(this.user));
195
322
  if (token) headers["X-User-Token"] = token;
196
323
  return headers;
197
324
  }
198
325
  /**
199
- * Parse JSON response text safely.
200
- * Returns `unknown` — callers must narrow before use.
201
- */
202
- parseJsonSafely(text, status) {
203
- try {
204
- return JSON.parse(text);
205
- } catch (parseError) {
206
- throw new RefletError(`Invalid response: ${parseError instanceof Error ? parseError.message : "Failed to parse JSON"}`, status);
207
- }
208
- }
209
- /**
210
- * Type guard for API error responses
211
- */
212
- isErrorResponse(data) {
213
- return typeof data === "object" && data !== null && "error" in data && typeof data.error === "string";
214
- }
215
- /**
216
- * Throw appropriate error based on status code
217
- */
218
- throwHttpError(message, status) {
219
- if (status === 401) throw new RefletAuthError(message);
220
- if (status === 404) throw new RefletNotFoundError(message);
221
- throw new RefletError(message, status);
222
- }
223
- /**
224
326
  * Make an authenticated API request
225
327
  */
226
328
  async request(method, path, body) {
227
329
  const response = await this.send(method, path, body);
228
- if (!response.ok) return this.throwResponseError(response);
330
+ if (!response.ok) return throwResponseError(response);
229
331
  const text = await response.text();
230
332
  if (!text) return {};
231
- return this.parseJsonSafely(text, response.status);
333
+ return parseJsonSafely(text, response.status);
232
334
  }
233
335
  async send(method, path, body) {
234
336
  try {
@@ -241,34 +343,8 @@ var Reflet = class {
241
343
  throw new RefletError(`Network error: ${networkError instanceof Error ? networkError.message : "Failed to connect"}`, 0);
242
344
  }
243
345
  }
244
- async throwResponseError(response) {
245
- const fallbackMessage = `Request failed with status ${response.status}`;
246
- const text = await response.text();
247
- if (!text) this.throwHttpError(fallbackMessage, response.status);
248
- const data = this.parseJsonSafely(text, response.status);
249
- const errorMessage = this.isErrorResponse(data) ? data.error : fallbackMessage;
250
- this.throwHttpError(errorMessage, response.status);
251
- }
252
- /**
253
- * Unsigned identity from the `user` option. The API attributes reports with
254
- * it but refuses voting, commenting and subscribing — those need `signUser`.
255
- */
256
- generateUserToken() {
257
- if (!this.user) return;
258
- const payload = {
259
- email: this.user.email,
260
- exp: Math.floor(Date.now() / 1e3) + 86400,
261
- iat: Math.floor(Date.now() / 1e3),
262
- id: this.user.id,
263
- name: this.user.name
264
- };
265
- return `${base64Utf8(JSON.stringify({
266
- alg: UNSIGNED_TOKEN_ALGORITHM,
267
- typ: "JWT"
268
- }))}.${base64Utf8(JSON.stringify(payload))}.`;
269
- }
270
346
  };
271
347
  //#endregion
272
- export { RefletNotFoundError as a, RefletError as i, Reflet as n, RefletValidationError as o, RefletAuthError as r, DEFAULT_API_URL as t };
348
+ export { RefletAuthError as a, RefletValidationError as c, surveyRegistryKey as i, Reflet as n, RefletError as o, surveyControllers as r, RefletNotFoundError as s, DEFAULT_API_URL as t };
273
349
 
274
- //# sourceMappingURL=client-DAdWdIFY.js.map
350
+ //# sourceMappingURL=client-oFFfRxAM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-oFFfRxAM.js","names":[],"sources":["../src/types.ts","../src/http-response.ts","../src/surveys/registry.ts","../../survey-core/src/respondent.ts","../src/surveys/survey-api.ts","../src/user-token.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 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 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 id: string;\n items: { id: string; status: FeedbackStatus; title: 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 { RefletAuthError, RefletError, RefletNotFoundError } from \"./types\";\n\n/** Returns `unknown`: callers narrow before use. */\nexport function 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\nfunction isErrorResponse(data: unknown): data is { error: string } {\n return (\n typeof data === \"object\" &&\n data !== null &&\n \"error\" in data &&\n typeof data.error === \"string\"\n );\n}\n\nfunction 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/** Throws the typed Reflet error for a failed response, using the API's `{ error }` message when present. */\nexport async function throwResponseError(response: Response): Promise<never> {\n const fallbackMessage = `Request failed with status ${response.status}`;\n const text = await response.text();\n if (!text) {\n throwHttpError(fallbackMessage, response.status);\n }\n\n const data = parseJsonSafely(text, response.status);\n throwHttpError(\n isErrorResponse(data) ? data.error : fallbackMessage,\n response.status\n );\n}\n","import type { SurveyController } from \"./controller\";\n\n/**\n * One survey controller per project (public key + API URL). Kept apart from\n * the controller so the plain client can signal submitted feedback without\n * bundling the survey runtime.\n */\nexport const surveyControllers = new Map<string, SurveyController>();\n\n/** `baseUrl` is the resolved API URL, so omitting it and passing the default match. */\nexport const surveyRegistryKey = (publicKey: string, baseUrl: string): string =>\n `${baseUrl}|${publicKey}`;\n","const RESPONDENT_ID_STORAGE_KEY = \"reflet:respondent-id\";\n\nlet memoryRespondentId: string | null = null;\n\n/** `crypto.randomUUID` only exists in secure contexts; plain-http intranet sites are not. */\nconst newRespondentId = (): string =>\n Array.from(crypto.getRandomValues(new Uint8Array(16)), (byte) =>\n byte.toString(16).padStart(2, \"0\")\n ).join(\"\");\n\n/**\n * A stable anonymous id per browser, so display frequency and sampling hold\n * across visits. Falls back to one id per page load when storage is blocked.\n */\nexport const getRespondentId = (): string => {\n try {\n const stored = window.localStorage.getItem(RESPONDENT_ID_STORAGE_KEY);\n if (stored) {\n return stored;\n }\n const created = newRespondentId();\n window.localStorage.setItem(RESPONDENT_ID_STORAGE_KEY, created);\n return created;\n } catch {\n memoryRespondentId ??= newRespondentId();\n return memoryRespondentId;\n }\n};\n","import type { AnswerValue, PublicSurvey } from \"@reflet/survey-core\";\nimport { getRespondentId } from \"@reflet/survey-core/client\";\n\nexport interface StartSurveyResponseParams {\n /** Page the survey was answered on. Defaults to the current page in a browser. */\n pageUrl?: string;\n surveyId: string;\n}\n\nexport interface SubmitSurveyAnswerParams {\n questionId: string;\n responseId: string;\n /** `null` clears an answer the respondent emptied. */\n value: AnswerValue | null;\n}\n\nexport interface StartSurveyResponseResult {\n responseId: string;\n}\n\nexport interface SubmitSurveyAnswerResult {\n answerId: string | null;\n}\n\nexport interface CompleteSurveyResponseResult {\n endingId: string;\n}\n\n/** The client's authenticated request, as the survey endpoints use it. */\nexport type ApiRequest = <Result>(\n method: \"GET\" | \"POST\",\n path: string,\n body?: unknown\n) => Promise<Result>;\n\n/** Respondent endpoints of the public survey API. */\nexport interface SurveyApi {\n complete: (responseId: string) => Promise<CompleteSurveyResponseResult>;\n dismiss: (responseId: string) => Promise<void>;\n eligible: () => Promise<PublicSurvey[]>;\n start: (\n params: StartSurveyResponseParams\n ) => Promise<StartSurveyResponseResult>;\n submitAnswer: (\n params: SubmitSurveyAnswerParams\n ) => Promise<SubmitSurveyAnswerResult>;\n}\n\n/** The anonymous browser id that keeps display frequency and sampling stable. */\nconst browserRespondentId = (): string | undefined =>\n typeof window === \"undefined\" ? undefined : getRespondentId();\n\nexport const createSurveyApi = (request: ApiRequest): SurveyApi => ({\n complete: async (responseId) => {\n const { endingId } = await request<CompleteSurveyResponseResult>(\n \"POST\",\n \"/api/v1/surveys/respond/complete\",\n { responseId }\n );\n return { endingId };\n },\n dismiss: async (responseId) => {\n await request(\"POST\", \"/api/v1/surveys/respond/dismiss\", { responseId });\n },\n eligible: async () => {\n const respondentId = browserRespondentId();\n const query = respondentId\n ? `?respondentId=${encodeURIComponent(respondentId)}`\n : \"\";\n const { surveys } = await request<{ surveys?: PublicSurvey[] }>(\n \"GET\",\n `/api/v1/surveys/eligible${query}`\n );\n return surveys ?? [];\n },\n start: ({ pageUrl, surveyId }) =>\n request(\"POST\", \"/api/v1/surveys/respond/start\", {\n pageUrl:\n pageUrl ??\n (typeof window === \"undefined\" ? undefined : window.location.href),\n respondentId: browserRespondentId(),\n surveyId,\n userAgent:\n typeof navigator === \"undefined\" ? undefined : navigator.userAgent,\n }),\n submitAnswer: (params) =>\n request(\"POST\", \"/api/v1/surveys/respond/answer\", params),\n});\n","import type { RefletUser } from \"./types\";\n\nconst UNSIGNED_TOKEN_ALGORITHM = \"none\";\nconst TOKEN_LIFETIME_SECONDS = 86_400;\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 * Unsigned identity from the `user` option. The API attributes reports with\n * it but refuses voting, commenting and subscribing — those need `signUser`.\n */\nexport function createUnsignedUserToken(user: RefletUser): string {\n const issuedAt = Math.floor(Date.now() / 1000);\n const payload = {\n email: user.email,\n exp: issuedAt + TOKEN_LIFETIME_SECONDS,\n iat: issuedAt,\n id: user.id,\n name: 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 return `${header}.${payloadB64}.`;\n}\n","import type { PublicSurvey } from \"@reflet/survey-core\";\nimport { parseJsonSafely, throwResponseError } from \"./http-response\";\nimport { surveyControllers, surveyRegistryKey } from \"./surveys/registry\";\nimport {\n createSurveyApi,\n type StartSurveyResponseParams,\n type SubmitSurveyAnswerParams,\n} from \"./surveys/survey-api\";\nimport {\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 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\";\nimport { createUnsignedUserToken } from \"./user-token\";\n\nexport const DEFAULT_API_URL = \"https://harmless-clam-802.convex.site\";\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 private readonly surveys = createSurveyApi((method, path, body) =>\n this.request(method, path, body)\n );\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 const created = await this.request<CreateFeedbackResponse>(\n \"POST\",\n \"/api/v1/feedback/create\",\n params\n );\n surveyControllers\n .get(surveyRegistryKey(this.publicKey, this.baseUrl))\n ?.notifyFeedbackSubmitted();\n return created;\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 /** Surveys this visitor may see now; schedule, cap, frequency and sampling are applied by the API. */\n getEligibleSurveys(): Promise<PublicSurvey[]> {\n return this.surveys.eligible();\n }\n\n /** Opens a response before the first answer is saved. */\n startSurveyResponse(params: StartSurveyResponseParams) {\n return this.surveys.start(params);\n }\n\n /** Saves one answer; `value: null` clears it. */\n submitSurveyAnswer(params: SubmitSurveyAnswerParams) {\n return this.surveys.submitAnswer(params);\n }\n\n /** Finishes a response; the API walks the flow and returns the ending reached. */\n completeSurveyResponse(responseId: string) {\n return this.surveys.complete(responseId);\n }\n\n /** Marks an unfinished response abandoned. */\n dismissSurveyResponse(responseId: string): Promise<void> {\n return this.surveys.dismiss(responseId);\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 =\n this.userToken ?? (this.user && createUnsignedUserToken(this.user));\n if (token) {\n headers[\"X-User-Token\"] = token;\n }\n\n return headers;\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 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 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"],"mappings":"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"}
@@ -0,0 +1,150 @@
1
+ import { b as SurveyQuestion, c as Answers, p as PublicSurvey, s as AnswerValue, t as Reflet, v as SurveyEnding } from "./index-Buum9tIY.js";
2
+ //#region ../survey-core/src/session.d.ts
3
+ /** How a session talks to Reflet. `value: null` clears an answer the respondent emptied. */
4
+ interface SurveyTransport {
5
+ answer: (input: {
6
+ questionId: string;
7
+ responseId: string;
8
+ value: AnswerValue | null;
9
+ }) => Promise<unknown>;
10
+ complete: (input: {
11
+ responseId: string;
12
+ }) => Promise<unknown>;
13
+ dismiss: (input: {
14
+ responseId: string;
15
+ }) => Promise<unknown>;
16
+ start: (input: {
17
+ surveyId: string;
18
+ }) => Promise<{
19
+ responseId: string;
20
+ }>;
21
+ }
22
+ interface SurveySessionCallbacks {
23
+ onAnswer?: (event: {
24
+ questionId: string;
25
+ surveyId: string;
26
+ value: AnswerValue;
27
+ }) => void;
28
+ onComplete?: (event: {
29
+ endingId: string;
30
+ responseId: string;
31
+ surveyId: string;
32
+ }) => void;
33
+ onDismiss?: (event: {
34
+ answeredCount: number;
35
+ responseId: string | null;
36
+ surveyId: string;
37
+ }) => void;
38
+ onStart?: (event: {
39
+ responseId: string;
40
+ surveyId: string;
41
+ }) => void;
42
+ }
43
+ type SurveySessionPhase = "starting" | "start_failed" | "question" | "ending" | "dismissed";
44
+ interface SurveySessionSnapshot {
45
+ answers: Answers;
46
+ canGoBack: boolean;
47
+ ending: SurveyEnding | null;
48
+ error: string | null;
49
+ /** The current answer leads straight to an ending, so the button submits. */
50
+ isLastStep: boolean;
51
+ isSubmitting: boolean;
52
+ phase: SurveySessionPhase;
53
+ /** 0–1, estimated along the path the current answers lead to. */
54
+ progress: number;
55
+ question: SurveyQuestion | null;
56
+ }
57
+ declare const previewTransport: SurveyTransport;
58
+ declare class SurveySession {
59
+ readonly survey: PublicSurvey;
60
+ private readonly transport;
61
+ private readonly callbacks;
62
+ private readonly endings;
63
+ private readonly listeners;
64
+ private readonly submitted;
65
+ private history;
66
+ private answers;
67
+ private current;
68
+ private ending;
69
+ private error;
70
+ private isSubmitting;
71
+ private phase;
72
+ private responseId;
73
+ private snapshot;
74
+ constructor(options: {
75
+ callbacks?: SurveySessionCallbacks;
76
+ survey: PublicSurvey;
77
+ transport: SurveyTransport;
78
+ });
79
+ readonly subscribe: (listener: () => void) => (() => void);
80
+ readonly getSnapshot: () => SurveySessionSnapshot;
81
+ /** A method, not a field read, so TS doesn't narrow `phase` across awaits. */
82
+ private isDismissed;
83
+ start(): Promise<void>;
84
+ setAnswer(value: AnswerValue | undefined): void;
85
+ next(): Promise<void>;
86
+ back(): void;
87
+ dismiss(): void;
88
+ private validate;
89
+ private saveAnswer;
90
+ private finish;
91
+ private update;
92
+ private buildSnapshot;
93
+ }
94
+ //#endregion
95
+ //#region src/surveys/use-survey-session.d.ts
96
+ /** A running survey: its current state plus the actions a UI needs. */
97
+ interface SurveySessionHandle {
98
+ back: () => void;
99
+ dismiss: () => void;
100
+ next: () => Promise<void>;
101
+ setAnswer: (value: AnswerValue | undefined) => void;
102
+ snapshot: SurveySessionSnapshot;
103
+ start: () => Promise<void>;
104
+ survey: PublicSurvey;
105
+ }
106
+ interface UseSurveySessionOptions {
107
+ /** Start the response on mount. Defaults to true. */
108
+ autoStart?: boolean;
109
+ callbacks?: SurveySessionCallbacks;
110
+ /**
111
+ * Where answers go. Defaults to the Reflet API inside `RefletProvider`,
112
+ * otherwise to `previewTransport`, which saves nothing.
113
+ */
114
+ transport?: SurveyTransport;
115
+ }
116
+ /** Subscribes a component to an existing session. */
117
+ declare function useSessionHandle(session: SurveySession): SurveySessionHandle;
118
+ /**
119
+ * Headless survey runner for custom UIs. A new session begins whenever
120
+ * `survey` or `transport` changes identity, so memoize them.
121
+ */
122
+ declare function useSurveySession(survey: PublicSurvey, options?: UseSurveySessionOptions): SurveySessionHandle;
123
+ //#endregion
124
+ //#region src/surveys/card/survey-card.d.ts
125
+ type SurveyCardVariant = "floating" | "inline" | "page";
126
+ type SurveyTheme = "auto" | "dark" | "light";
127
+ interface SurveyCardProps {
128
+ className?: string;
129
+ /** Shows a close button. Closing mid-survey dismisses the response first. */
130
+ onClose?: () => void;
131
+ /** Accent for buttons, selections and progress. Any CSS color. */
132
+ primaryColor?: string;
133
+ session: SurveySessionHandle;
134
+ theme?: SurveyTheme;
135
+ variant?: SurveyCardVariant;
136
+ }
137
+ /**
138
+ * Renders any survey session: questions, branching, endings and errors.
139
+ * Ships its own scoped styles, so it works in a shadow root or a page.
140
+ */
141
+ declare function SurveyCard({ className, onClose, primaryColor, session, theme, variant }: SurveyCardProps): import("react").JSX.Element | null;
142
+ //#endregion
143
+ //#region src/surveys/client-transport.d.ts
144
+ /** Runs a survey session against the Reflet REST API with this client's key and user. */
145
+ declare const createRefletSurveyTransport: (client: Reflet, options?: {
146
+ pageUrl?: string;
147
+ }) => SurveyTransport;
148
+ //#endregion
149
+ export { SurveyTheme as a, useSessionHandle as c, SurveySessionCallbacks as d, SurveySessionPhase as f, previewTransport as h, SurveyCardVariant as i, useSurveySession as l, SurveyTransport as m, SurveyCard as n, SurveySessionHandle as o, SurveySessionSnapshot as p, SurveyCardProps as r, UseSurveySessionOptions as s, createRefletSurveyTransport as t, SurveySession as u };
150
+ //# sourceMappingURL=client-transport-B7BdVU7J.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-transport-B7BdVU7J.d.ts","names":[],"sources":["../../survey-core/src/session.ts","../src/surveys/use-survey-session.ts","../src/surveys/card/survey-card.tsx","../src/surveys/client-transport.ts"],"mappings":";;;UAYiB;EACf,SAAS;IACP;IACA;IACA,OAAO;QACH;EACN,WAAW;IAAS;QAAyB;EAC7C,UAAU;IAAS;QAAyB;EAC5C,QAAQ;IAAS;QAAuB;IAAU;;;UAGnC;EACf,YAAY;IACV;IACA;IACA,OAAO;;EAET,cAAc;IACZ;IACA;IACA;;EAEF,aAAa;IACX;IACA;IACA;;EAEF,WAAW;IAAS;IAAoB;;;KAG9B;UAOK;EACf,SAAS;EACT;EACA,QAAQ;EACR;;EAEA;EACA;EACA,OAAO;;EAEP;EACA,UAAU;;cAOC,kBAAkB;cAOlB;WACF,QAAQ;mBACA;mBACA;mBACA;mBACA;mBACA;UACT;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;EAER,YAAY;IACV,YAAY;IACZ,QAAQ;IACR,WAAW;;WASJ,YAAS;WAKT,mBAAkB;;UAGnB;EAIF,SAAS;EA6Bf,UAAU,OAAO;EAcX,QAAQ;EAsCd;EAUA;UAoBQ;UAaM;UAgCA;UAmBN;UAoBA;;;;;UCnSO;EACf;EACA;EACA,YAAY;EACZ,YAAY,OAAO;EACnB,UAAU;EACV,aAAa;EACb,QAAQ;;UAGO;;EAEf;EACA,YAAY;;;;;EAKZ,YAAY;;;iBAME,iBAAiB,SAAS,gBAAgB;;;;;iBAwB1C,iBACd,QAAQ,cACR,UAAS,0BACR;;;KCxDS;KACA;UAEK;EACf;;EAEA;;EAEA;EACA,SAAS;EACT,QAAQ;EACR,UAAU;;;;;;iBA8CI,aACd,WACA,SACA,cACA,SACA,OACA,WACC,kCAAe,IAAA;;;;cC1EL,8BAA2B,QAC9B,QAAM;EACH;MACV"}