@sentry/junior-dashboard 0.58.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/LICENSE +201 -0
- package/dist/app.d.ts +22 -0
- package/dist/app.js +492 -0
- package/dist/auth.d.ts +30 -0
- package/dist/client/App.d.ts +2 -0
- package/dist/client/api.d.ts +10 -0
- package/dist/client/code.d.ts +15 -0
- package/dist/client/components/CommandRail.d.ts +6 -0
- package/dist/client/components/ConversationList.d.ts +7 -0
- package/dist/client/components/ConversationRowStats.d.ts +6 -0
- package/dist/client/components/ConversationStack.d.ts +5 -0
- package/dist/client/components/ConversationSummary.d.ts +5 -0
- package/dist/client/components/EmptyTelemetry.d.ts +5 -0
- package/dist/client/components/FilterTabs.d.ts +6 -0
- package/dist/client/components/JuniorLogo.d.ts +2 -0
- package/dist/client/components/LoadingView.d.ts +4 -0
- package/dist/client/components/Section.d.ts +6 -0
- package/dist/client/components/SectionHeader.d.ts +6 -0
- package/dist/client/components/SectionTitle.d.ts +5 -0
- package/dist/client/components/StatusBadge.d.ts +6 -0
- package/dist/client/components/ToolFrame.d.ts +10 -0
- package/dist/client/components/Transcript.d.ts +5 -0
- package/dist/client/components/TranscriptHeader.d.ts +7 -0
- package/dist/client/components/TranscriptLoading.d.ts +2 -0
- package/dist/client/components/TranscriptText.d.ts +6 -0
- package/dist/client/components/TranscriptToolView.d.ts +9 -0
- package/dist/client/components/TranscriptTurn.d.ts +8 -0
- package/dist/client/components/TurnDurationChart.d.ts +6 -0
- package/dist/client/components/statusStyles.d.ts +3 -0
- package/dist/client/components/transcriptPreview.d.ts +4 -0
- package/dist/client/components/transcriptRenderModel.d.ts +30 -0
- package/dist/client/components/transcriptStyles.d.ts +4 -0
- package/dist/client/format.d.ts +64 -0
- package/dist/client/pages/CommandCenter.d.ts +6 -0
- package/dist/client/pages/ConversationPage.d.ts +5 -0
- package/dist/client/pages/ConversationsPage.d.ts +5 -0
- package/dist/client/styles.d.ts +2 -0
- package/dist/client/types.d.ts +153 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +56012 -0
- package/dist/config.d.ts +4 -0
- package/dist/handler.d.ts +2 -0
- package/dist/handler.js +563 -0
- package/dist/nitro.d.ts +16 -0
- package/dist/nitro.js +80 -0
- package/dist/tailwind.css +2 -0
- package/package.json +60 -0
- package/src/app.ts +421 -0
- package/src/auth.ts +202 -0
- package/src/client/App.tsx +175 -0
- package/src/client/api.ts +99 -0
- package/src/client/code.tsx +139 -0
- package/src/client/components/CommandRail.tsx +74 -0
- package/src/client/components/ConversationList.tsx +94 -0
- package/src/client/components/ConversationRowStats.tsx +21 -0
- package/src/client/components/ConversationStack.tsx +67 -0
- package/src/client/components/ConversationSummary.tsx +40 -0
- package/src/client/components/EmptyTelemetry.tsx +11 -0
- package/src/client/components/FilterTabs.tsx +35 -0
- package/src/client/components/JuniorLogo.tsx +8 -0
- package/src/client/components/LoadingView.tsx +16 -0
- package/src/client/components/Section.tsx +17 -0
- package/src/client/components/SectionHeader.tsx +16 -0
- package/src/client/components/SectionTitle.tsx +10 -0
- package/src/client/components/StatusBadge.tsx +40 -0
- package/src/client/components/ToolFrame.tsx +52 -0
- package/src/client/components/Transcript.tsx +39 -0
- package/src/client/components/TranscriptHeader.tsx +52 -0
- package/src/client/components/TranscriptLoading.tsx +10 -0
- package/src/client/components/TranscriptText.tsx +45 -0
- package/src/client/components/TranscriptToolView.tsx +207 -0
- package/src/client/components/TranscriptTurn.tsx +487 -0
- package/src/client/components/TurnDurationChart.tsx +349 -0
- package/src/client/components/statusStyles.ts +9 -0
- package/src/client/components/transcriptPreview.ts +20 -0
- package/src/client/components/transcriptRenderModel.ts +193 -0
- package/src/client/components/transcriptStyles.ts +11 -0
- package/src/client/format.ts +628 -0
- package/src/client/pages/CommandCenter.tsx +37 -0
- package/src/client/pages/ConversationPage.tsx +145 -0
- package/src/client/pages/ConversationsPage.tsx +59 -0
- package/src/client/styles.ts +6 -0
- package/src/client/types.ts +153 -0
- package/src/client.tsx +79 -0
- package/src/config.ts +72 -0
- package/src/handler.ts +26 -0
- package/src/nitro.ts +110 -0
- package/src/tailwind.css +13 -0
- package/src/virtual-modules.d.ts +5 -0
package/dist/app.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// src/app.ts
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { existsSync, readFileSync } from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { createJuniorReporting } from "@sentry/junior/reporting";
|
|
6
|
+
import { initSentry } from "@sentry/junior/instrumentation";
|
|
7
|
+
|
|
8
|
+
// src/auth.ts
|
|
9
|
+
import { betterAuth } from "better-auth";
|
|
10
|
+
var DEFAULT_SESSION_MAX_AGE_SECONDS = 60 * 60 * 8;
|
|
11
|
+
function sanitizeDashboardSession(session) {
|
|
12
|
+
const { email, emailVerified, hostedDomain, name } = session.user;
|
|
13
|
+
return {
|
|
14
|
+
user: {
|
|
15
|
+
email,
|
|
16
|
+
emailVerified,
|
|
17
|
+
hostedDomain,
|
|
18
|
+
name
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function required(value, name) {
|
|
23
|
+
if (!value?.trim()) {
|
|
24
|
+
throw new Error(`${name} is required for Junior dashboard auth`);
|
|
25
|
+
}
|
|
26
|
+
return value.trim();
|
|
27
|
+
}
|
|
28
|
+
function firstHostedDomain(domains) {
|
|
29
|
+
return domains.length === 1 ? domains[0] : void 0;
|
|
30
|
+
}
|
|
31
|
+
function withHttps(host) {
|
|
32
|
+
return /^https?:\/\//.test(host) ? host : `https://${host}`;
|
|
33
|
+
}
|
|
34
|
+
function stripTrailingSlashes(value) {
|
|
35
|
+
let end = value.length;
|
|
36
|
+
while (end > 1 && value.charCodeAt(end - 1) === 47) {
|
|
37
|
+
end -= 1;
|
|
38
|
+
}
|
|
39
|
+
return end === value.length ? value : value.slice(0, end);
|
|
40
|
+
}
|
|
41
|
+
function resolveBaseURL(config) {
|
|
42
|
+
const explicit = config.baseURL ?? process.env.BETTER_AUTH_URL ?? process.env.JUNIOR_BASE_URL;
|
|
43
|
+
if (explicit?.trim()) {
|
|
44
|
+
return stripTrailingSlashes(withHttps(explicit.trim()));
|
|
45
|
+
}
|
|
46
|
+
const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
|
|
47
|
+
if (vercelProd) {
|
|
48
|
+
return stripTrailingSlashes(withHttps(vercelProd));
|
|
49
|
+
}
|
|
50
|
+
const vercelUrl = process.env.VERCEL_URL?.trim();
|
|
51
|
+
if (vercelUrl) {
|
|
52
|
+
return stripTrailingSlashes(withHttps(vercelUrl));
|
|
53
|
+
}
|
|
54
|
+
return "http://localhost:3000";
|
|
55
|
+
}
|
|
56
|
+
function createDashboardAuth(config) {
|
|
57
|
+
const secret = required(
|
|
58
|
+
config.secret ?? process.env.BETTER_AUTH_SECRET ?? process.env.JUNIOR_SECRET,
|
|
59
|
+
"JUNIOR_SECRET or BETTER_AUTH_SECRET"
|
|
60
|
+
);
|
|
61
|
+
const baseURL = resolveBaseURL(config);
|
|
62
|
+
const googleClientId = required(
|
|
63
|
+
config.googleClientId ?? process.env.GOOGLE_CLIENT_ID,
|
|
64
|
+
"GOOGLE_CLIENT_ID"
|
|
65
|
+
);
|
|
66
|
+
const googleClientSecret = required(
|
|
67
|
+
config.googleClientSecret ?? process.env.GOOGLE_CLIENT_SECRET,
|
|
68
|
+
"GOOGLE_CLIENT_SECRET"
|
|
69
|
+
);
|
|
70
|
+
const auth = betterAuth({
|
|
71
|
+
appName: "Junior Dashboard",
|
|
72
|
+
baseURL,
|
|
73
|
+
basePath: config.authPath,
|
|
74
|
+
secret,
|
|
75
|
+
trustedOrigins: config.trustedOrigins,
|
|
76
|
+
socialProviders: {
|
|
77
|
+
google: {
|
|
78
|
+
clientId: googleClientId,
|
|
79
|
+
clientSecret: googleClientSecret,
|
|
80
|
+
hd: config.googleHostedDomain,
|
|
81
|
+
prompt: "select_account",
|
|
82
|
+
mapProfileToUser(profile) {
|
|
83
|
+
return {
|
|
84
|
+
email: profile.email,
|
|
85
|
+
emailVerified: profile.email_verified,
|
|
86
|
+
hostedDomain: profile.hd,
|
|
87
|
+
image: profile.picture,
|
|
88
|
+
name: profile.name
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
user: {
|
|
94
|
+
additionalFields: {
|
|
95
|
+
hostedDomain: {
|
|
96
|
+
type: "string",
|
|
97
|
+
required: false,
|
|
98
|
+
input: false,
|
|
99
|
+
returned: true
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
account: {
|
|
104
|
+
storeStateStrategy: "cookie",
|
|
105
|
+
storeAccountCookie: false,
|
|
106
|
+
updateAccountOnSignIn: false
|
|
107
|
+
},
|
|
108
|
+
session: {
|
|
109
|
+
expiresIn: config.sessionMaxAgeSeconds ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
110
|
+
disableSessionRefresh: true,
|
|
111
|
+
cookieCache: {
|
|
112
|
+
enabled: true,
|
|
113
|
+
strategy: "jwe",
|
|
114
|
+
maxAge: config.sessionMaxAgeSeconds ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
115
|
+
refreshCache: false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
handler(request) {
|
|
121
|
+
return auth.handler(request);
|
|
122
|
+
},
|
|
123
|
+
async getSession(request) {
|
|
124
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
125
|
+
if (!session) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return sanitizeDashboardSession(session);
|
|
129
|
+
},
|
|
130
|
+
async signInWithGoogle(request, callbackURL) {
|
|
131
|
+
const result = await auth.api.signInSocial({
|
|
132
|
+
body: {
|
|
133
|
+
provider: "google",
|
|
134
|
+
callbackURL
|
|
135
|
+
},
|
|
136
|
+
headers: request.headers,
|
|
137
|
+
returnHeaders: true
|
|
138
|
+
});
|
|
139
|
+
if (!("url" in result.response) || !result.response.url) {
|
|
140
|
+
throw new Error("Google sign-in did not return a redirect URL");
|
|
141
|
+
}
|
|
142
|
+
result.headers.set("location", result.response.url);
|
|
143
|
+
return new Response(null, {
|
|
144
|
+
status: 302,
|
|
145
|
+
headers: result.headers
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function resolveGoogleHostedDomainHint(domains) {
|
|
151
|
+
return firstHostedDomain(domains.map((domain) => domain.toLowerCase()));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/app.ts
|
|
155
|
+
var DEFAULT_BASE_PATH = "/";
|
|
156
|
+
var DEFAULT_AUTH_PATH = "/api/auth";
|
|
157
|
+
var DASHBOARD_CLIENT_VERSION = Date.now().toString(36);
|
|
158
|
+
function hasSentryConversationLinks() {
|
|
159
|
+
return Boolean(
|
|
160
|
+
process.env.SENTRY_DSN?.trim() && process.env.SENTRY_ORG_SLUG?.trim()
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
function normalizePath(path2, fallback) {
|
|
164
|
+
const value = path2.trim() || fallback;
|
|
165
|
+
const withSlash = value.startsWith("/") ? value : `/${value}`;
|
|
166
|
+
return stripTrailingSlashes2(withSlash);
|
|
167
|
+
}
|
|
168
|
+
function stripTrailingSlashes2(value) {
|
|
169
|
+
let end = value.length;
|
|
170
|
+
while (end > 1 && value.charCodeAt(end - 1) === 47) {
|
|
171
|
+
end -= 1;
|
|
172
|
+
}
|
|
173
|
+
return end === value.length ? value : value.slice(0, end);
|
|
174
|
+
}
|
|
175
|
+
function normalizeValues(values) {
|
|
176
|
+
return [
|
|
177
|
+
...new Set(
|
|
178
|
+
(values ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
179
|
+
)
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
function isJsonRoute(pathname) {
|
|
183
|
+
return pathname.startsWith("/api/");
|
|
184
|
+
}
|
|
185
|
+
function dashboardLoginUrl(request) {
|
|
186
|
+
const url = new URL(request.url);
|
|
187
|
+
url.pathname = "/api/dashboard/login";
|
|
188
|
+
url.search = "";
|
|
189
|
+
return url.toString();
|
|
190
|
+
}
|
|
191
|
+
function callbackUrl(request, basePath) {
|
|
192
|
+
const url = new URL(request.url);
|
|
193
|
+
url.pathname = basePath;
|
|
194
|
+
url.search = "";
|
|
195
|
+
return url.toString();
|
|
196
|
+
}
|
|
197
|
+
function isAuthorized(session, allowedDomains, allowedEmails) {
|
|
198
|
+
const email = session.user.email?.toLowerCase();
|
|
199
|
+
const domain = session.user.hostedDomain?.toLowerCase();
|
|
200
|
+
if (session.user.emailVerified && email && allowedEmails.includes(email)) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
return Boolean(
|
|
204
|
+
session.user.emailVerified && domain && allowedDomains.includes(domain)
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
function unauthorized(request) {
|
|
208
|
+
if (isJsonRoute(new URL(request.url).pathname)) {
|
|
209
|
+
return Response.json({ error: "unauthenticated" }, { status: 401 });
|
|
210
|
+
}
|
|
211
|
+
return Response.redirect(dashboardLoginUrl(request), 302);
|
|
212
|
+
}
|
|
213
|
+
function forbidden(request) {
|
|
214
|
+
if (!isJsonRoute(new URL(request.url).pathname)) {
|
|
215
|
+
return new Response(
|
|
216
|
+
`<!DOCTYPE html>
|
|
217
|
+
<html lang="en">
|
|
218
|
+
<head>
|
|
219
|
+
<meta charset="utf-8" />
|
|
220
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
221
|
+
<title>Junior access denied</title>
|
|
222
|
+
<style>
|
|
223
|
+
${readDashboardTailwind()}
|
|
224
|
+
</style>
|
|
225
|
+
</head>
|
|
226
|
+
<body class="m-0 bg-black font-sans text-white [color-scheme:dark]">
|
|
227
|
+
<main class="grid min-h-screen place-items-center p-8">
|
|
228
|
+
<section class="max-w-lg border-l-4 border-rose-400 pl-4">
|
|
229
|
+
<h1 class="m-0 mb-3 text-[1.75rem] font-bold leading-tight">Access denied</h1>
|
|
230
|
+
<p class="m-0 leading-relaxed text-[#b8b8b8]">Your Google account is authenticated, but it is not allowed to use this Junior dashboard.</p>
|
|
231
|
+
</section>
|
|
232
|
+
</main>
|
|
233
|
+
</body>
|
|
234
|
+
</html>`,
|
|
235
|
+
{
|
|
236
|
+
headers: {
|
|
237
|
+
"cache-control": "no-store",
|
|
238
|
+
"content-type": "text/html; charset=utf-8"
|
|
239
|
+
},
|
|
240
|
+
status: 403
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return Response.json({ error: "forbidden" }, { status: 403 });
|
|
245
|
+
}
|
|
246
|
+
function dashboardSessionBypass() {
|
|
247
|
+
return {
|
|
248
|
+
user: {
|
|
249
|
+
email: "local-dashboard@localhost",
|
|
250
|
+
emailVerified: true,
|
|
251
|
+
hostedDomain: "localhost"
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function readDashboardAsset(fileName) {
|
|
256
|
+
const localDistUrl = new URL(`./${fileName}`, import.meta.url);
|
|
257
|
+
if (existsSync(localDistUrl)) {
|
|
258
|
+
return readFileSync(localDistUrl, "utf8");
|
|
259
|
+
}
|
|
260
|
+
const sourceDistUrl = new URL(`../dist/${fileName}`, import.meta.url);
|
|
261
|
+
if (existsSync(sourceDistUrl)) {
|
|
262
|
+
return readFileSync(sourceDistUrl, "utf8");
|
|
263
|
+
}
|
|
264
|
+
const workspacePackagePath = path.join(
|
|
265
|
+
process.cwd(),
|
|
266
|
+
"node_modules",
|
|
267
|
+
"@sentry",
|
|
268
|
+
"junior-dashboard",
|
|
269
|
+
"dist",
|
|
270
|
+
fileName
|
|
271
|
+
);
|
|
272
|
+
if (existsSync(workspacePackagePath)) {
|
|
273
|
+
return readFileSync(workspacePackagePath, "utf8");
|
|
274
|
+
}
|
|
275
|
+
return "";
|
|
276
|
+
}
|
|
277
|
+
function readDashboardClient() {
|
|
278
|
+
const client = readDashboardAsset("client.js");
|
|
279
|
+
if (!client) {
|
|
280
|
+
throw new Error("Junior dashboard client bundle was not found");
|
|
281
|
+
}
|
|
282
|
+
return client;
|
|
283
|
+
}
|
|
284
|
+
function dashboardTimeZone() {
|
|
285
|
+
return process.env.JUNIOR_TIMEZONE || "America/Los_Angeles";
|
|
286
|
+
}
|
|
287
|
+
function readDashboardTailwind() {
|
|
288
|
+
return readDashboardAsset("tailwind.css");
|
|
289
|
+
}
|
|
290
|
+
function dashboardPagePaths(basePath) {
|
|
291
|
+
return [
|
|
292
|
+
basePath,
|
|
293
|
+
basePath === "/" ? "/conversations" : `${basePath}/conversations`,
|
|
294
|
+
basePath === "/" ? "/sessions" : `${basePath}/sessions`
|
|
295
|
+
];
|
|
296
|
+
}
|
|
297
|
+
function renderDashboard(basePath) {
|
|
298
|
+
return new Response(
|
|
299
|
+
`<!DOCTYPE html>
|
|
300
|
+
<html lang="en">
|
|
301
|
+
<head>
|
|
302
|
+
<meta charset="utf-8" />
|
|
303
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
304
|
+
<title>Junior</title>
|
|
305
|
+
<style>
|
|
306
|
+
${readDashboardTailwind()}
|
|
307
|
+
</style>
|
|
308
|
+
</head>
|
|
309
|
+
<body class="m-0 bg-black text-white [color-scheme:dark]">
|
|
310
|
+
<div id="dashboard-root"></div>
|
|
311
|
+
<script>
|
|
312
|
+
window.__JUNIOR_DASHBOARD_BASE_PATH__ = ${JSON.stringify(basePath)};
|
|
313
|
+
(function () {
|
|
314
|
+
function escapeHtml(value) {
|
|
315
|
+
return String(value)
|
|
316
|
+
.replace(/&/g, "&")
|
|
317
|
+
.replace(/</g, "<")
|
|
318
|
+
.replace(/>/g, ">")
|
|
319
|
+
.replace(/"/g, """)
|
|
320
|
+
.replace(/'/g, "'");
|
|
321
|
+
}
|
|
322
|
+
function errorText(error) {
|
|
323
|
+
if (!error) return "Unknown dashboard error";
|
|
324
|
+
if (typeof error === "string") return error;
|
|
325
|
+
if (error.stack) return error.stack;
|
|
326
|
+
if (error.message) return error.message;
|
|
327
|
+
try {
|
|
328
|
+
return JSON.stringify(error, null, 2);
|
|
329
|
+
} catch (_error) {
|
|
330
|
+
return String(error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
window.__JUNIOR_DASHBOARD_SHOW_ERROR__ = function (error) {
|
|
334
|
+
var root = document.getElementById("dashboard-root");
|
|
335
|
+
if (!root) return;
|
|
336
|
+
root.innerHTML =
|
|
337
|
+
'<main class="grid min-h-screen place-items-center bg-black p-8 text-white">' +
|
|
338
|
+
'<section class="w-full max-w-5xl border border-rose-400/50 bg-[#0b0b0b] p-5 font-sans">' +
|
|
339
|
+
'<div class="font-mono text-xs uppercase leading-none text-[#888]">Dashboard Error</div>' +
|
|
340
|
+
'<h1 class="mt-2 text-3xl font-bold leading-tight tracking-normal">Junior failed to render</h1>' +
|
|
341
|
+
'<p class="my-4 max-w-3xl text-[#b8b8b8]">The dashboard hit a client-side exception. The stack trace is shown here so the page does not fail blank.</p>' +
|
|
342
|
+
'<pre class="max-h-[60vh] overflow-auto whitespace-pre-wrap break-words border border-white/10 bg-black p-4 font-mono text-sm leading-relaxed text-white">' +
|
|
343
|
+
escapeHtml(errorText(error)) +
|
|
344
|
+
"</pre></section></main>";
|
|
345
|
+
};
|
|
346
|
+
window.addEventListener("error", function (event) {
|
|
347
|
+
window.__JUNIOR_DASHBOARD_SHOW_ERROR__(event.error || event.message);
|
|
348
|
+
});
|
|
349
|
+
window.addEventListener("unhandledrejection", function (event) {
|
|
350
|
+
window.__JUNIOR_DASHBOARD_SHOW_ERROR__(event.reason);
|
|
351
|
+
});
|
|
352
|
+
})();
|
|
353
|
+
</script>
|
|
354
|
+
<script type="module" src="/api/dashboard/client.js?v=${DASHBOARD_CLIENT_VERSION}"></script>
|
|
355
|
+
</body>
|
|
356
|
+
</html>`,
|
|
357
|
+
{
|
|
358
|
+
headers: {
|
|
359
|
+
"cache-control": "no-store",
|
|
360
|
+
"content-type": "text/html; charset=utf-8"
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
function renderFavicon() {
|
|
366
|
+
return new Response(
|
|
367
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="#000000"/><text x="16" y="20.5" fill="#ffffff" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="11" font-weight="900" text-anchor="middle">Jr</text></svg>`,
|
|
368
|
+
{ headers: { "content-type": "image/svg+xml" } }
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
function createDashboardApp(options) {
|
|
372
|
+
if (process.env.SENTRY_DSN?.trim()) {
|
|
373
|
+
initSentry();
|
|
374
|
+
}
|
|
375
|
+
const basePath = normalizePath(
|
|
376
|
+
options.basePath ?? DEFAULT_BASE_PATH,
|
|
377
|
+
DEFAULT_BASE_PATH
|
|
378
|
+
);
|
|
379
|
+
const authPath = normalizePath(
|
|
380
|
+
options.authPath ?? DEFAULT_AUTH_PATH,
|
|
381
|
+
DEFAULT_AUTH_PATH
|
|
382
|
+
);
|
|
383
|
+
const allowedDomains = normalizeValues(options.allowedGoogleDomains);
|
|
384
|
+
const allowedEmails = normalizeValues(options.allowedEmails);
|
|
385
|
+
const authRequired = options.authRequired !== false;
|
|
386
|
+
if (authRequired && allowedDomains.length === 0 && allowedEmails.length === 0) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
"Junior dashboard auth requires allowedGoogleDomains or allowedEmails"
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const auth = authRequired ? options.auth ?? createDashboardAuth({
|
|
392
|
+
authPath,
|
|
393
|
+
trustedOrigins: options.trustedOrigins ?? [],
|
|
394
|
+
googleHostedDomain: resolveGoogleHostedDomainHint(allowedDomains),
|
|
395
|
+
sessionMaxAgeSeconds: options.sessionMaxAgeSeconds
|
|
396
|
+
}) : void 0;
|
|
397
|
+
const reporting = options.reporting ?? createJuniorReporting();
|
|
398
|
+
const app = new Hono();
|
|
399
|
+
if (auth) {
|
|
400
|
+
app.on(["GET", "POST"], `${authPath}/*`, (c) => auth.handler(c.req.raw));
|
|
401
|
+
}
|
|
402
|
+
app.get("/favicon.ico", () => renderFavicon());
|
|
403
|
+
app.get("/api/dashboard/login", async (c) => {
|
|
404
|
+
if (!auth) {
|
|
405
|
+
return Response.redirect(callbackUrl(c.req.raw, basePath), 302);
|
|
406
|
+
}
|
|
407
|
+
return auth.signInWithGoogle(c.req.raw, callbackUrl(c.req.raw, basePath));
|
|
408
|
+
});
|
|
409
|
+
const requireDashboardSession = async (c, next) => {
|
|
410
|
+
if (!authRequired) {
|
|
411
|
+
c.set("dashboardSession", dashboardSessionBypass());
|
|
412
|
+
await next();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (!auth) {
|
|
416
|
+
return unauthorized(c.req.raw);
|
|
417
|
+
}
|
|
418
|
+
const session = await auth.getSession(c.req.raw);
|
|
419
|
+
if (!session) {
|
|
420
|
+
return unauthorized(c.req.raw);
|
|
421
|
+
}
|
|
422
|
+
if (!isAuthorized(session, allowedDomains, allowedEmails)) {
|
|
423
|
+
return forbidden(c.req.raw);
|
|
424
|
+
}
|
|
425
|
+
c.set("dashboardSession", sanitizeDashboardSession(session));
|
|
426
|
+
await next();
|
|
427
|
+
};
|
|
428
|
+
if (basePath === "/") {
|
|
429
|
+
app.use("/*", requireDashboardSession);
|
|
430
|
+
} else {
|
|
431
|
+
app.use(basePath, requireDashboardSession);
|
|
432
|
+
app.use(`${basePath}/*`, requireDashboardSession);
|
|
433
|
+
}
|
|
434
|
+
app.use("/api/dashboard/*", requireDashboardSession);
|
|
435
|
+
for (const path2 of dashboardPagePaths(basePath)) {
|
|
436
|
+
app.get(path2, () => renderDashboard(basePath));
|
|
437
|
+
if (path2 !== "/") {
|
|
438
|
+
app.get(`${path2}/*`, () => renderDashboard(basePath));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
app.get("/api/dashboard/health", async () => {
|
|
442
|
+
return Response.json(await reporting.getHealth());
|
|
443
|
+
});
|
|
444
|
+
app.get("/api/dashboard/runtime", async () => {
|
|
445
|
+
return Response.json(await reporting.getRuntimeInfo());
|
|
446
|
+
});
|
|
447
|
+
app.get("/api/dashboard/plugins", async () => {
|
|
448
|
+
return Response.json(await reporting.getPlugins());
|
|
449
|
+
});
|
|
450
|
+
app.get("/api/dashboard/skills", async () => {
|
|
451
|
+
return Response.json(await reporting.getSkills());
|
|
452
|
+
});
|
|
453
|
+
app.get("/api/dashboard/sessions", async () => {
|
|
454
|
+
return Response.json(await reporting.getSessions());
|
|
455
|
+
});
|
|
456
|
+
app.get("/api/dashboard/conversations/:conversationId", async (c) => {
|
|
457
|
+
return Response.json(
|
|
458
|
+
await reporting.getConversation(
|
|
459
|
+
decodeURIComponent(c.req.param("conversationId"))
|
|
460
|
+
)
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
app.get("/api/dashboard/config", () => {
|
|
464
|
+
return Response.json({
|
|
465
|
+
allowedEmailCount: allowedEmails.length,
|
|
466
|
+
allowedGoogleDomainCount: allowedDomains.length,
|
|
467
|
+
authRequired,
|
|
468
|
+
authPath,
|
|
469
|
+
basePath,
|
|
470
|
+
sentryConversationLinks: hasSentryConversationLinks(),
|
|
471
|
+
timeZone: dashboardTimeZone()
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
app.get("/api/dashboard/me", (c) => {
|
|
475
|
+
return Response.json(c.get("dashboardSession"));
|
|
476
|
+
});
|
|
477
|
+
app.get("/api/dashboard/info", async () => {
|
|
478
|
+
return Response.json(await reporting.getRuntimeInfo());
|
|
479
|
+
});
|
|
480
|
+
app.get("/api/dashboard/client.js", () => {
|
|
481
|
+
return new Response(readDashboardClient(), {
|
|
482
|
+
headers: {
|
|
483
|
+
"cache-control": "no-store",
|
|
484
|
+
"content-type": "application/javascript; charset=utf-8"
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
return app;
|
|
489
|
+
}
|
|
490
|
+
export {
|
|
491
|
+
createDashboardApp
|
|
492
|
+
};
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface DashboardUser {
|
|
2
|
+
email?: string | null;
|
|
3
|
+
emailVerified?: boolean;
|
|
4
|
+
hostedDomain?: string | null;
|
|
5
|
+
name?: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface DashboardSession {
|
|
8
|
+
user: DashboardUser;
|
|
9
|
+
}
|
|
10
|
+
export interface DashboardAuthConfig {
|
|
11
|
+
baseURL?: string;
|
|
12
|
+
authPath: string;
|
|
13
|
+
trustedOrigins: string[];
|
|
14
|
+
secret?: string;
|
|
15
|
+
googleClientId?: string;
|
|
16
|
+
googleClientSecret?: string;
|
|
17
|
+
googleHostedDomain?: string;
|
|
18
|
+
sessionMaxAgeSeconds?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface DashboardAuth {
|
|
21
|
+
handler(request: Request): Promise<Response>;
|
|
22
|
+
getSession(request: Request): Promise<DashboardSession | null>;
|
|
23
|
+
signInWithGoogle(request: Request, callbackURL: string): Promise<Response>;
|
|
24
|
+
}
|
|
25
|
+
/** Keep dashboard identity responses limited to user display fields. */
|
|
26
|
+
export declare function sanitizeDashboardSession(session: DashboardSession): DashboardSession;
|
|
27
|
+
/** Create the Better Auth bridge used by dashboard browser routes. */
|
|
28
|
+
export declare function createDashboardAuth(config: DashboardAuthConfig): DashboardAuth;
|
|
29
|
+
/** Resolve a Google hosted-domain login hint when it is unambiguous. */
|
|
30
|
+
export declare function resolveGoogleHostedDomainHint(domains: string[]): string | undefined;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
2
|
+
import type { ConversationDetailFeed, DashboardData } from "./types";
|
|
3
|
+
/** Share dashboard query cache between route data and tooltip detail lookups. */
|
|
4
|
+
export declare const client: QueryClient;
|
|
5
|
+
/** Poll the dashboard summary feed used by command center and conversation lists. */
|
|
6
|
+
export declare function useDashboardData(): import("@tanstack/react-query").UseQueryResult<NoInfer<DashboardData>, Error>;
|
|
7
|
+
/** Poll one conversation transcript while preserving route-level disabled state. */
|
|
8
|
+
export declare function useConversationData(conversationId: string | undefined): import("@tanstack/react-query").UseQueryResult<NoInfer<ConversationDetailFeed>, Error>;
|
|
9
|
+
/** Read one conversation transcript payload for dashboard-local detail views. */
|
|
10
|
+
export declare function readConversationData(conversationId: string): Promise<ConversationDetailFeed>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type BundledLanguage } from "shiki/bundle/web";
|
|
2
|
+
import type { CodeBlock } from "./types";
|
|
3
|
+
/** Count rendered children so transcripts can decide which markup node expands. */
|
|
4
|
+
export declare function countStructuredBlockChildren(block: CodeBlock): number;
|
|
5
|
+
/** Render structured markup blocks as collapsible nodes instead of flat code. */
|
|
6
|
+
export declare function StructuredMarkup(props: {
|
|
7
|
+
block: CodeBlock;
|
|
8
|
+
firstChildIndex: number;
|
|
9
|
+
lastChildIndex: number;
|
|
10
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
/** Render highlighted code while keeping Shiki output responsive in transcripts. */
|
|
12
|
+
export declare function HighlightedCode(props: {
|
|
13
|
+
code: string;
|
|
14
|
+
language: BundledLanguage;
|
|
15
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DashboardData } from "../types";
|
|
2
|
+
/** Render the command-center summary rail from the dashboard health payload. */
|
|
3
|
+
export declare function CommandRail(props: {
|
|
4
|
+
data?: DashboardData;
|
|
5
|
+
error: Error | null;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Conversation } from "../types";
|
|
2
|
+
/** Render the full conversation table used by the conversations page. */
|
|
3
|
+
export declare function ConversationList(props: {
|
|
4
|
+
conversations: Conversation[];
|
|
5
|
+
selectedId?: string;
|
|
6
|
+
search?: string;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SessionFilter } from "../types";
|
|
2
|
+
/** Render conversation filters while keeping URL state owned by the page. */
|
|
3
|
+
export declare function FilterTabs(props: {
|
|
4
|
+
current: SessionFilter;
|
|
5
|
+
onChange(filter: SessionFilter): void;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { VisualStatus } from "../types";
|
|
2
|
+
/** Render readable status text while keeping severity color restrained. */
|
|
3
|
+
export declare function StatusBadge(props: {
|
|
4
|
+
label?: string;
|
|
5
|
+
status: VisualStatus | undefined;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/** Render the shared expandable/non-expandable frame for transcript tools. */
|
|
3
|
+
export declare function ToolFrame(props: {
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
meta: string[];
|
|
6
|
+
raw?: boolean;
|
|
7
|
+
signature: ReactNode;
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
/** Provide the shared transcript tool-frame shell for nonstandard part views. */
|
|
10
|
+
export declare function toolFrameClass(): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
2
|
+
/** Render transcript controls without coupling them to turn rendering. */
|
|
3
|
+
export declare function TranscriptHeader(props: {
|
|
4
|
+
onChange(value: TranscriptViewMode): void;
|
|
5
|
+
redacted: boolean;
|
|
6
|
+
value: TranscriptViewMode;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|