reflet-sdk 0.7.3 → 0.7.5
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 +28 -0
- package/README.md +54 -17
- package/dist/desktop.d.ts +24 -0
- package/dist/desktop.d.ts.map +1 -0
- package/dist/desktop.js +118 -0
- package/dist/desktop.js.map +1 -0
- package/dist/{devtools-layer-CsIcpepI.js → devtools-layer-o1O55wu1.js} +396 -277
- package/dist/devtools-layer-o1O55wu1.js.map +1 -0
- package/dist/devtools-next.d.ts.map +1 -1
- package/dist/devtools-next.js +1 -1
- package/dist/{sessions-panel-BYM0UUgT.js → devtools-styles-TWR5Ps34.js} +2078 -1686
- package/dist/devtools-styles-TWR5Ps34.js.map +1 -0
- package/dist/devtools-vite.js +2 -1
- package/dist/devtools-vite.js.map +1 -1
- package/dist/feedback.d.ts +7 -3
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +21 -12
- package/dist/feedback.js.map +1 -1
- package/dist/{handler-17FmwoSX.js → handler-ndF3gZBx.js} +11 -416
- package/dist/handler-ndF3gZBx.js.map +1 -0
- package/dist/reflet-proxy-BW6q9xds.js +467 -0
- package/dist/reflet-proxy-BW6q9xds.js.map +1 -0
- package/dist/{submit-Dl0rm6TV.js → submit-DJIKxRUp.js} +2 -2
- package/dist/submit-DJIKxRUp.js.map +1 -0
- package/dist/{team-layer-Dvjkh-_y.js → team-layer-_It1_oSo.js} +159 -95
- package/dist/team-layer-_It1_oSo.js.map +1 -0
- package/dist/team-tools-bridge-C51PpkUI.d.ts +31 -0
- package/dist/team-tools-bridge-C51PpkUI.d.ts.map +1 -0
- package/package.json +5 -1
- package/dist/devtools-layer-CsIcpepI.js.map +0 -1
- package/dist/handler-17FmwoSX.js.map +0 -1
- package/dist/sessions-panel-BYM0UUgT.js.map +0 -1
- package/dist/submit-Dl0rm6TV.js.map +0 -1
- package/dist/team-layer-Dvjkh-_y.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ 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.7.5] - 2026-10-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Team tools in desktop apps: `createTeamToolsHost` from `reflet-sdk/desktop` runs them in an Electron main process, and `<RefletFeedback teamTools={bridge} />` takes the bridge it returns. Pages loaded from `file://` can now record sessions: sign-in opens the system browser and returns to a loopback listener, and the token stays in `~/.reflet/devtools/`, out of the page.
|
|
13
|
+
|
|
14
|
+
## [0.7.4] - 2026-10-15
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Devtools: one panel button replaces the separate Board and Notes buttons. It reopens the panel on the tab you used last, even after a reload, and shows how many notes aren't on the board yet. The microphone opens Sessions directly. The bar steps aside while the panel is open, and the panel opens wider (480 px).
|
|
19
|
+
- Devtools: the panel's tabs carry icons, in the order Board, Notes, Sessions, Code. Your Reflet account (avatar, name, organization) sits in the panel's footer, with Connect and Disconnect; the Board tab no longer has a connection bar, and Sessions no longer says "Recording as…".
|
|
20
|
+
- Devtools: secondary choices are menus, not a second row of tabs: This page / Whole board on the Board, On screen / Details on a session's intentions. Every menu and surface follows one radius scale.
|
|
21
|
+
- Devtools: opening a session replaces the tabs with a back button to Sessions; the brief adds one more level back to the session. The links back inside the content, except "All evidence" from one screenshot, are gone. Moving between levels puts keyboard focus on the new view's heading, or on the view itself while it loads, and the session keeps its tab and On screen / Details choice when you come back from the brief.
|
|
22
|
+
- Sessions: on first use, "What a session records" shows above Start session instead of below it. Deleting an intention or a screenshot now says exactly which saved briefs go with it.
|
|
23
|
+
- Devtools: when connecting isn't possible, the panel says what it needs: a public key and a local address (localhost, *.localhost or *.test), or REFLET_SECRET_KEY. Error messages point to the panel's footer instead of the old Board tab.
|
|
24
|
+
- Sessions: "Hand off N confirmed" is now "Prepare brief". Nothing is shared until you copy the brief or download its `.zip`; "Copied or exported briefs can't be recalled" is gone. Earlier briefs list the intention they carried, when, and whether you copied or downloaded them. Dates read "Today, 14:02", "Yesterday, 14:02" or "12 Oct, 14:02".
|
|
25
|
+
- Notes: "Send to board" is now "Add to board". The empty Notes tab has a Pick an element button.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Sessions: add an intention to the board from its card, or every confirmed intention not on the board yet from the review's footer. Intentions on the board show "On the board".
|
|
30
|
+
- Devtools: the account footer shows your Reflet avatar. Requires the matching Reflet backend; older backends show initials.
|
|
31
|
+
|
|
32
|
+
### Removed
|
|
33
|
+
|
|
34
|
+
- Devtools: the pick button in the panel header (the bar and the Notes tab have it), and the press-shrink on rows and cards.
|
|
35
|
+
|
|
8
36
|
## [0.7.3] - 2026-10-14
|
|
9
37
|
|
|
10
38
|
### Added
|
package/README.md
CHANGED
|
@@ -167,7 +167,7 @@ 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. |
|
|
170
|
+
| `teamTools` | `boolean \| TeamToolsBridge` | `true` | Lets signed-in members of your Reflet organization record sessions in production; see below. Desktop apps pass a bridge. |
|
|
171
171
|
| `appUrl` | `string` | `"https://www.reflet.app"` | Reflet web app origin team tools sign in on. |
|
|
172
172
|
| `onSubmit` | `(result: { feedbackId: string }) => void` | — | Fires after a successful send. |
|
|
173
173
|
|
|
@@ -180,13 +180,16 @@ canvas content may come out blank.
|
|
|
180
180
|
#### Devtools for your team (development only)
|
|
181
181
|
|
|
182
182
|
In `NODE_ENV=development`, `RefletFeedback` also mounts a small devtools bar
|
|
183
|
-
above its launcher
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
183
|
+
above its launcher: **Pick**, **Sessions** (the microphone) and the panel
|
|
184
|
+
button, which reopens the panel on the tab you used last. Drag the bar by its
|
|
185
|
+
grip anywhere, double-click the grip to put it back. While the panel is open
|
|
186
|
+
the bar steps aside. The panel opens full height: drag its header to move it
|
|
187
|
+
(double-click to dock it back), drag its inner edge, bottom edge or corner to
|
|
188
|
+
resize it (double-click a handle to reset). Position and size are remembered
|
|
189
|
+
and refit whenever the window changes size. Your Reflet account and
|
|
190
|
+
organization sit in the panel's footer, with Connect and Disconnect. Production
|
|
191
|
+
builds drop it at bundle time — the devtools chunk is never shipped to your
|
|
192
|
+
users. Pass `devtools={false}` to turn it off.
|
|
190
193
|
|
|
191
194
|
- **Pick** — click an element to write a note on it. The note card has a
|
|
192
195
|
"View code" button, and Shift+click (or Shift+Enter) skips the note and opens
|
|
@@ -204,24 +207,27 @@ turn it off.
|
|
|
204
207
|
to retry.
|
|
205
208
|
- **Copy as prompt** — every note, with its source location, component stack,
|
|
206
209
|
selector and markup, as one Markdown prompt for a coding agent.
|
|
207
|
-
- **
|
|
210
|
+
- **Add to board** — notes become internal feedback: members see them with an
|
|
208
211
|
"Internal" badge, the public board never does, and an admin can make one
|
|
209
212
|
public from the dashboard.
|
|
210
|
-
- **Board** — the board's feedback for the current route, or the whole board
|
|
211
|
-
with "Show on page", the reporter's
|
|
212
|
-
the element's text.
|
|
213
|
+
- **Board** — the board's feedback for the current route, or the whole board
|
|
214
|
+
(the view menu next to the count), with "Show on page", the reporter's
|
|
215
|
+
source location, and a code search for the element's text.
|
|
213
216
|
- **Sessions** — talk through the app while you use it: Reflet records your
|
|
214
217
|
voice, what you point at and mark, and a screenshot at the start, at each
|
|
215
218
|
mark and on each page change. Finish, and Reflet transcribes the session and
|
|
216
|
-
proposes the
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
proposes the intentions behind what you said. Confirm, edit or dismiss each
|
|
220
|
+
one. Like notes, confirmed intentions have two ways out: **Prepare brief**
|
|
221
|
+
builds a brief you copy into a coding agent or download as a `.zip` with its
|
|
222
|
+
screenshots (nothing is shared until you copy or download it), and **Add to
|
|
223
|
+
board** turns them into internal feedback, one by one or all at once.
|
|
224
|
+
Sessions are recorded under your own name, so they need Connect rather than
|
|
225
|
+
`REFLET_SECRET_KEY`.
|
|
220
226
|
|
|
221
227
|
Code view and the board go through one dev-only route that reads files from
|
|
222
228
|
your repository and holds the board credential server-side.
|
|
223
229
|
|
|
224
|
-
To reach the board, click **Connect to Reflet** in the
|
|
230
|
+
To reach the board, click **Connect to Reflet** in the panel's footer: sign in on
|
|
225
231
|
reflet.app, approve the organization that owns the widget's key, and the dev
|
|
226
232
|
server keeps a revocable token in `~/.reflet/devtools/`,
|
|
227
233
|
outside the repository. That token acts as you, only reaches the devtools
|
|
@@ -293,6 +299,37 @@ transcript is saved, within 24 hours at most. Pausing stops all collection;
|
|
|
293
299
|
keystrokes are never recorded, form fields appear empty in screenshots, and
|
|
294
300
|
password fields and elements marked `data-reflet-redact` are hidden from them.
|
|
295
301
|
|
|
302
|
+
##### Desktop apps (Electron)
|
|
303
|
+
|
|
304
|
+
A page loaded from `file://` has no origin Reflet can bind a sign-in to, so the
|
|
305
|
+
browser flow above can't work there. Run team tools from the app's main
|
|
306
|
+
process instead: sign-in opens the system browser and comes back to a one-shot
|
|
307
|
+
listener on `127.0.0.1`, like the devtools' Connect, and the token stays on disk
|
|
308
|
+
in `~/.reflet/devtools/`, never in the page. Members approve it on reflet.app as
|
|
309
|
+
a local connection; revoke it from Account → Devtools. The key needs no allowed
|
|
310
|
+
domain for this.
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
// main process
|
|
314
|
+
import { app, ipcMain, shell } from 'electron';
|
|
315
|
+
import { createTeamToolsHost } from 'reflet-sdk/desktop';
|
|
316
|
+
|
|
317
|
+
const teamTools = createTeamToolsHost({
|
|
318
|
+
appDataDirectory: app.getPath('userData'),
|
|
319
|
+
openExternal: (url) => shell.openExternal(url),
|
|
320
|
+
publicKey: 'fb_pub_xxx',
|
|
321
|
+
});
|
|
322
|
+
ipcMain.handle('reflet-team-tools:account', () => teamTools.account());
|
|
323
|
+
ipcMain.handle('reflet-team-tools:request', (_event, call) => teamTools.request(call));
|
|
324
|
+
ipcMain.handle('reflet-team-tools:sign-in', () => teamTools.signIn());
|
|
325
|
+
ipcMain.handle('reflet-team-tools:sign-out', () => teamTools.signOut());
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Expose the four calls from the preload script, then pass them as the bridge:
|
|
329
|
+
`<RefletFeedback publicKey="fb_pub_xxx" teamTools={bridge} />`. Team tools show
|
|
330
|
+
at once, so only pass the bridge in builds meant for your team. `request` only
|
|
331
|
+
forwards the session and feedback endpoints the devtools use.
|
|
332
|
+
|
|
296
333
|
### 5. In-App Surveys
|
|
297
334
|
|
|
298
335
|
Create surveys in the Reflet dashboard (NPS, ratings, choices, text, statements,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { i as TeamToolsReply, n as TeamToolsBridge, r as TeamToolsCall, t as TeamToolsAccount } from "./team-tools-bridge-C51PpkUI.js";
|
|
2
|
+
//#region src/devtools/server/desktop.d.ts
|
|
3
|
+
export interface TeamToolsHostOptions {
|
|
4
|
+
/** Reflet API origin. Defaults to the hosted API. */
|
|
5
|
+
apiUrl?: string;
|
|
6
|
+
/** A directory only this app uses, such as Electron's `app.getPath("userData")`. Keeps its sign-in apart from other apps and dev servers. */
|
|
7
|
+
appDataDirectory: string;
|
|
8
|
+
/** Reflet web app origin sign-in happens on. Defaults to https://www.reflet.app. */
|
|
9
|
+
appUrl?: string;
|
|
10
|
+
/** Opens a URL in the system browser, such as Electron's `shell.openExternal`. */
|
|
11
|
+
openExternal: (url: string) => Promise<void>;
|
|
12
|
+
publicKey: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Runs team tools for a desktop app in its privileged process (Electron's
|
|
16
|
+
* main): sign-in goes through the system browser back to a one-shot loopback
|
|
17
|
+
* listener, and the token stays on disk outside the page, like the dev
|
|
18
|
+
* server's Connect. Expose the returned bridge to the page over IPC and pass it
|
|
19
|
+
* to `<RefletFeedback teamTools={bridge} />`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createTeamToolsHost(options: TeamToolsHostOptions): TeamToolsBridge;
|
|
22
|
+
//#endregion
|
|
23
|
+
export type { TeamToolsAccount, TeamToolsBridge, TeamToolsCall, TeamToolsReply };
|
|
24
|
+
//# sourceMappingURL=desktop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"desktop.d.ts","names":[],"sources":["../src/devtools/server/desktop.ts"],"mappings":";;iBA4BiB;;EAEf;;EAEA;;EAEA;;EAEA,eAAe,gBAAgB;EAC/B;;;;;;;;;wBAsGc,oBACd,SAAS,uBACR"}
|
package/dist/desktop.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { a as connectCallbackUriFor, f as PENDING_CONNECT_TTL_MS, i as connectAuthorizeUrl, l as forgetConnection, m as readConnection, o as connectOutcomePage, p as htmlPage, r as resolveDevtoolsOptions, t as proxyWithBoardCredential, u as redeemConnectCode } from "./reflet-proxy-BW6q9xds.js";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
//#region src/devtools/server/desktop.ts
|
|
4
|
+
const LOOPBACK_HOST = "127.0.0.1";
|
|
5
|
+
const SIGN_IN_TIMED_OUT = "Reflet sign-in timed out. Sign in again.";
|
|
6
|
+
async function sendPage(response, page) {
|
|
7
|
+
response.writeHead(page.status, Object.fromEntries(page.headers));
|
|
8
|
+
response.end(await page.text());
|
|
9
|
+
}
|
|
10
|
+
async function listenOnLoopback(server) {
|
|
11
|
+
await new Promise((resolve, reject) => {
|
|
12
|
+
server.once("error", reject);
|
|
13
|
+
server.listen(0, LOOPBACK_HOST, resolve);
|
|
14
|
+
});
|
|
15
|
+
const address = server.address();
|
|
16
|
+
if (address === null || typeof address === "string") throw new Error("The Reflet sign-in listener has no port.");
|
|
17
|
+
return address.port;
|
|
18
|
+
}
|
|
19
|
+
/** Settles on the first callback carrying this sign-in's state; anything else gets a 404 and changes nothing. */
|
|
20
|
+
function awaitConnectCallback(server, pending) {
|
|
21
|
+
const callbackPath = new URL(pending.redirectUri).pathname;
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error(SIGN_IN_TIMED_OUT)), PENDING_CONNECT_TTL_MS);
|
|
24
|
+
let isRedeeming = false;
|
|
25
|
+
const redeem = async (url, response) => {
|
|
26
|
+
const outcome = await redeemConnectCode(pending.settings, {
|
|
27
|
+
code: url.searchParams.get("code"),
|
|
28
|
+
redirectUri: pending.redirectUri,
|
|
29
|
+
state: pending.state
|
|
30
|
+
});
|
|
31
|
+
await sendPage(response, connectOutcomePage(outcome));
|
|
32
|
+
if (!outcome.ok) throw new Error(outcome.message);
|
|
33
|
+
return { organizationName: outcome.organizationName };
|
|
34
|
+
};
|
|
35
|
+
server.on("request", (request, response) => {
|
|
36
|
+
const url = new URL(request.url ?? "/", pending.redirectUri);
|
|
37
|
+
const isThisSignIn = request.method === "GET" && url.pathname === callbackPath && url.searchParams.get("state") === pending.state;
|
|
38
|
+
if (isRedeeming || !isThisSignIn) {
|
|
39
|
+
sendPage(response, htmlPage("Not found", "Nothing here answers this link.", 404)).catch(() => void 0);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
isRedeeming = true;
|
|
43
|
+
clearTimeout(timeout);
|
|
44
|
+
redeem(url, response).then(resolve, reject);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function startBrowserSignIn(settings, publicKey) {
|
|
49
|
+
const server = createServer();
|
|
50
|
+
const port = await listenOnLoopback(server);
|
|
51
|
+
const redirectUri = connectCallbackUriFor(`http://${LOOPBACK_HOST}:${port}`);
|
|
52
|
+
const { authorizeUrl, state } = connectAuthorizeUrl(settings, {
|
|
53
|
+
publicKey,
|
|
54
|
+
redirectUri
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
account: awaitConnectCallback(server, {
|
|
58
|
+
redirectUri,
|
|
59
|
+
settings,
|
|
60
|
+
state
|
|
61
|
+
}).finally(() => server.close()),
|
|
62
|
+
authorizeUrl
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Runs team tools for a desktop app in its privileged process (Electron's
|
|
67
|
+
* main): sign-in goes through the system browser back to a one-shot loopback
|
|
68
|
+
* listener, and the token stays on disk outside the page, like the dev
|
|
69
|
+
* server's Connect. Expose the returned bridge to the page over IPC and pass it
|
|
70
|
+
* to `<RefletFeedback teamTools={bridge} />`.
|
|
71
|
+
*/
|
|
72
|
+
function createTeamToolsHost(options) {
|
|
73
|
+
const settings = resolveDevtoolsOptions({
|
|
74
|
+
apiUrl: options.apiUrl,
|
|
75
|
+
appUrl: options.appUrl,
|
|
76
|
+
root: options.appDataDirectory
|
|
77
|
+
}, {});
|
|
78
|
+
let pendingSignIn = null;
|
|
79
|
+
const currentSignIn = () => {
|
|
80
|
+
if (pendingSignIn) return pendingSignIn;
|
|
81
|
+
const started = startBrowserSignIn(settings, options.publicKey);
|
|
82
|
+
pendingSignIn = started;
|
|
83
|
+
const release = () => {
|
|
84
|
+
if (pendingSignIn === started) pendingSignIn = null;
|
|
85
|
+
};
|
|
86
|
+
started.then((signIn) => signIn.account).then(release, release);
|
|
87
|
+
return started;
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
account: async () => {
|
|
91
|
+
const connection = await readConnection(settings.root, settings.apiUrl);
|
|
92
|
+
return connection && { organizationName: connection.organizationName };
|
|
93
|
+
},
|
|
94
|
+
request: async (call) => {
|
|
95
|
+
const search = call.query ? `?${new URLSearchParams(call.query)}` : "";
|
|
96
|
+
const response = await proxyWithBoardCredential(new Request(`${settings.apiUrl}/${search}`, {
|
|
97
|
+
body: call.method === "POST" ? call.body : void 0,
|
|
98
|
+
method: call.method
|
|
99
|
+
}), call.path, settings);
|
|
100
|
+
return {
|
|
101
|
+
body: await response.text(),
|
|
102
|
+
status: response.status
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
signIn: async () => {
|
|
106
|
+
const signIn = await currentSignIn();
|
|
107
|
+
await options.openExternal(signIn.authorizeUrl);
|
|
108
|
+
return await signIn.account;
|
|
109
|
+
},
|
|
110
|
+
signOut: async () => {
|
|
111
|
+
await forgetConnection(settings);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
export { createTeamToolsHost };
|
|
117
|
+
|
|
118
|
+
//# sourceMappingURL=desktop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"desktop.js","names":[],"sources":["../src/devtools/server/desktop.ts"],"sourcesContent":["import { createServer, type Server, type ServerResponse } from \"node:http\";\nimport type {\n TeamToolsAccount,\n TeamToolsBridge,\n} from \"../../sessions/team/team-tools-bridge\";\nimport {\n connectAuthorizeUrl,\n connectCallbackUriFor,\n connectOutcomePage,\n forgetConnection,\n redeemConnectCode,\n} from \"./connect/connect-routes\";\nimport { readConnection } from \"./connect/connection-store\";\nimport { htmlPage } from \"./connect/html-page\";\nimport { PENDING_CONNECT_TTL_MS } from \"./connect/pending-connects\";\nimport {\n type ResolvedDevtoolsOptions,\n resolveDevtoolsOptions,\n} from \"./options\";\nimport { proxyWithBoardCredential } from \"./reflet-proxy\";\n\nexport type {\n TeamToolsAccount,\n TeamToolsBridge,\n TeamToolsCall,\n TeamToolsReply,\n} from \"../../sessions/team/team-tools-bridge\";\n\nexport interface TeamToolsHostOptions {\n /** Reflet API origin. Defaults to the hosted API. */\n apiUrl?: string;\n /** A directory only this app uses, such as Electron's `app.getPath(\"userData\")`. Keeps its sign-in apart from other apps and dev servers. */\n appDataDirectory: string;\n /** Reflet web app origin sign-in happens on. Defaults to https://www.reflet.app. */\n appUrl?: string;\n /** Opens a URL in the system browser, such as Electron's `shell.openExternal`. */\n openExternal: (url: string) => Promise<void>;\n publicKey: string;\n}\n\ninterface BrowserSignIn {\n account: Promise<TeamToolsAccount>;\n authorizeUrl: string;\n}\n\nconst LOOPBACK_HOST = \"127.0.0.1\";\nconst SIGN_IN_TIMED_OUT = \"Reflet sign-in timed out. Sign in again.\";\n\nasync function sendPage(response: ServerResponse, page: Response) {\n response.writeHead(page.status, Object.fromEntries(page.headers));\n response.end(await page.text());\n}\n\nasync function listenOnLoopback(server: Server): Promise<number> {\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(0, LOOPBACK_HOST, resolve);\n });\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n throw new Error(\"The Reflet sign-in listener has no port.\");\n }\n return address.port;\n}\n\n/** Settles on the first callback carrying this sign-in's state; anything else gets a 404 and changes nothing. */\nfunction awaitConnectCallback(\n server: Server,\n pending: {\n redirectUri: string;\n settings: ResolvedDevtoolsOptions;\n state: string;\n }\n): Promise<TeamToolsAccount> {\n const callbackPath = new URL(pending.redirectUri).pathname;\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(\n () => reject(new Error(SIGN_IN_TIMED_OUT)),\n PENDING_CONNECT_TTL_MS\n );\n let isRedeeming = false;\n const redeem = async (url: URL, response: ServerResponse) => {\n const outcome = await redeemConnectCode(pending.settings, {\n code: url.searchParams.get(\"code\"),\n redirectUri: pending.redirectUri,\n state: pending.state,\n });\n await sendPage(response, connectOutcomePage(outcome));\n if (!outcome.ok) {\n throw new Error(outcome.message);\n }\n return { organizationName: outcome.organizationName };\n };\n server.on(\"request\", (request, response) => {\n const url = new URL(request.url ?? \"/\", pending.redirectUri);\n const isThisSignIn =\n request.method === \"GET\" &&\n url.pathname === callbackPath &&\n url.searchParams.get(\"state\") === pending.state;\n if (isRedeeming || !isThisSignIn) {\n sendPage(\n response,\n htmlPage(\"Not found\", \"Nothing here answers this link.\", 404)\n ).catch(() => undefined);\n return;\n }\n isRedeeming = true;\n clearTimeout(timeout);\n redeem(url, response).then(resolve, reject);\n });\n });\n}\n\nasync function startBrowserSignIn(\n settings: ResolvedDevtoolsOptions,\n publicKey: string\n): Promise<BrowserSignIn> {\n const server = createServer();\n const port = await listenOnLoopback(server);\n const redirectUri = connectCallbackUriFor(`http://${LOOPBACK_HOST}:${port}`);\n const { authorizeUrl, state } = connectAuthorizeUrl(settings, {\n publicKey,\n redirectUri,\n });\n const account = awaitConnectCallback(server, {\n redirectUri,\n settings,\n state,\n }).finally(() => server.close());\n return { account, authorizeUrl };\n}\n\n/**\n * Runs team tools for a desktop app in its privileged process (Electron's\n * main): sign-in goes through the system browser back to a one-shot loopback\n * listener, and the token stays on disk outside the page, like the dev\n * server's Connect. Expose the returned bridge to the page over IPC and pass it\n * to `<RefletFeedback teamTools={bridge} />`.\n */\nexport function createTeamToolsHost(\n options: TeamToolsHostOptions\n): TeamToolsBridge {\n const settings = resolveDevtoolsOptions(\n {\n apiUrl: options.apiUrl,\n appUrl: options.appUrl,\n root: options.appDataDirectory,\n },\n {}\n );\n let pendingSignIn: Promise<BrowserSignIn> | null = null;\n\n const currentSignIn = (): Promise<BrowserSignIn> => {\n if (pendingSignIn) {\n return pendingSignIn;\n }\n const started = startBrowserSignIn(settings, options.publicKey);\n pendingSignIn = started;\n const release = () => {\n if (pendingSignIn === started) {\n pendingSignIn = null;\n }\n };\n started.then((signIn) => signIn.account).then(release, release);\n return started;\n };\n\n return {\n account: async () => {\n const connection = await readConnection(settings.root, settings.apiUrl);\n return connection && { organizationName: connection.organizationName };\n },\n request: async (call) => {\n const search = call.query ? `?${new URLSearchParams(call.query)}` : \"\";\n const response = await proxyWithBoardCredential(\n new Request(`${settings.apiUrl}/${search}`, {\n body: call.method === \"POST\" ? call.body : undefined,\n method: call.method,\n }),\n call.path,\n settings\n );\n return { body: await response.text(), status: response.status };\n },\n signIn: async () => {\n const signIn = await currentSignIn();\n await options.openExternal(signIn.authorizeUrl);\n return await signIn.account;\n },\n signOut: async () => {\n await forgetConnection(settings);\n },\n };\n}\n"],"mappings":"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"}
|