astryx-dracula 0.1.3 → 0.2.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/BRAND.md +1 -1
- package/README.md +2 -0
- package/USAGE.md +13 -0
- package/astryx-dracula.js +4 -4
- package/astryx.integration.mjs +3 -0
- package/package.json +4 -2
- package/templates/ai-chat-landing.template.mjs +6 -0
- package/templates/ai-chat-landing.tsx +476 -0
- package/templates/ai-chat.template.mjs +6 -0
- package/templates/ai-chat.tsx +762 -0
- package/templates/blank.template.mjs +6 -0
- package/templates/blank.tsx +16 -0
- package/templates/centered-hero.template.mjs +6 -0
- package/templates/centered-hero.tsx +188 -0
- package/templates/classic-gallery.template.mjs +6 -0
- package/templates/classic-gallery.tsx +181 -0
- package/templates/contact-form.template.mjs +6 -0
- package/templates/contact-form.tsx +292 -0
- package/templates/dashboard-portfolio.template.mjs +6 -0
- package/templates/dashboard-portfolio.tsx +800 -0
- package/templates/dashboard.template.mjs +6 -0
- package/templates/dashboard.tsx +774 -0
- package/templates/detail-page.template.mjs +6 -0
- package/templates/detail-page.tsx +695 -0
- package/templates/documentation-design.template.mjs +6 -0
- package/templates/documentation-design.tsx +751 -0
- package/templates/documentation-technical.template.mjs +6 -0
- package/templates/documentation-technical.tsx +292 -0
- package/templates/documentation.template.mjs +6 -0
- package/templates/documentation.tsx +347 -0
- package/templates/editor.template.mjs +6 -0
- package/templates/editor.tsx +959 -0
- package/templates/file-explorer.template.mjs +6 -0
- package/templates/file-explorer.tsx +577 -0
- package/templates/form-two-column.template.mjs +6 -0
- package/templates/form-two-column.tsx +314 -0
- package/templates/gallery-hero.template.mjs +6 -0
- package/templates/gallery-hero.tsx +287 -0
- package/templates/ide.template.mjs +6 -0
- package/templates/ide.tsx +533 -0
- package/templates/incident-console.template.mjs +6 -0
- package/templates/incident-console.tsx +584 -0
- package/templates/kanban-board.template.mjs +6 -0
- package/templates/kanban-board.tsx +746 -0
- package/templates/library.template.mjs +6 -0
- package/templates/library.tsx +489 -0
- package/templates/login-card.template.mjs +6 -0
- package/templates/login-card.tsx +200 -0
- package/templates/login-split.template.mjs +6 -0
- package/templates/login-split.tsx +318 -0
- package/templates/login-sso.template.mjs +6 -0
- package/templates/login-sso.tsx +277 -0
- package/templates/login.template.mjs +6 -0
- package/templates/login.tsx +98 -0
- package/templates/messaging-shell.template.mjs +6 -0
- package/templates/messaging-shell.tsx +675 -0
- package/templates/mixed-gallery.template.mjs +6 -0
- package/templates/mixed-gallery.tsx +190 -0
- package/templates/payment-form.template.mjs +6 -0
- package/templates/payment-form.tsx +990 -0
- package/templates/product-detail.template.mjs +6 -0
- package/templates/product-detail.tsx +366 -0
- package/templates/product-gallery.template.mjs +6 -0
- package/templates/product-gallery.tsx +173 -0
- package/templates/settings-dialog.template.mjs +6 -0
- package/templates/settings-dialog.tsx +920 -0
- package/templates/settings-sidebar.template.mjs +6 -0
- package/templates/settings-sidebar.tsx +952 -0
- package/templates/settings.template.mjs +6 -0
- package/templates/settings.tsx +235 -0
- package/templates/shell-nav.template.mjs +6 -0
- package/templates/shell-nav.tsx +306 -0
- package/templates/shell-side-nav.template.mjs +6 -0
- package/templates/shell-side-nav.tsx +238 -0
- package/templates/shell-top-nav.template.mjs +6 -0
- package/templates/shell-top-nav.tsx +243 -0
- package/templates/side-gallery.template.mjs +6 -0
- package/templates/side-gallery.tsx +179 -0
- package/templates/table-grouped.template.mjs +6 -0
- package/templates/table-grouped.tsx +1174 -0
- package/templates/table-page-chart.template.mjs +6 -0
- package/templates/table-page-chart.tsx +693 -0
- package/templates/table-page-heatmap-status.template.mjs +6 -0
- package/templates/table-page-heatmap-status.tsx +547 -0
- package/templates/table-page-shoe-store-heatmap.template.mjs +6 -0
- package/templates/table-page-shoe-store-heatmap.tsx +1038 -0
- package/templates/table-page.template.mjs +6 -0
- package/templates/table-page.tsx +445 -0
- package/templates/table.template.mjs +6 -0
- package/templates/table.tsx +84 -0
- package/templates/theme-showcase.template.mjs +6 -0
- package/templates/theme-showcase.tsx +1282 -0
- package/theme.css +4 -4
- package/tokens.css +4 -4
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {useRef, useState, type CSSProperties} from 'react';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
HStack,
|
|
7
|
+
VStack,
|
|
8
|
+
StackItem,
|
|
9
|
+
Layout,
|
|
10
|
+
LayoutContent,
|
|
11
|
+
} from '@astryxdesign/core/Layout';
|
|
12
|
+
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
13
|
+
import {
|
|
14
|
+
ChatComposer,
|
|
15
|
+
ChatComposerInput,
|
|
16
|
+
ChatLayout,
|
|
17
|
+
ChatMessage,
|
|
18
|
+
ChatMessageBubble,
|
|
19
|
+
ChatMessageList,
|
|
20
|
+
ChatMessageMetadata,
|
|
21
|
+
ChatSystemMessage,
|
|
22
|
+
ChatTokenizedText,
|
|
23
|
+
ChatToolCalls,
|
|
24
|
+
} from '@astryxdesign/core/Chat';
|
|
25
|
+
import {Avatar} from '@astryxdesign/core/Avatar';
|
|
26
|
+
import {Card} from '@astryxdesign/core/Card';
|
|
27
|
+
import {ClickableCard} from '@astryxdesign/core/ClickableCard';
|
|
28
|
+
import {Section} from '@astryxdesign/core/Section';
|
|
29
|
+
import {Markdown} from '@astryxdesign/core/Markdown';
|
|
30
|
+
import {CodeBlock} from '@astryxdesign/core/CodeBlock';
|
|
31
|
+
import {Timestamp} from '@astryxdesign/core/Timestamp';
|
|
32
|
+
import {Token} from '@astryxdesign/core/Token';
|
|
33
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
34
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
35
|
+
import {Dialog, DialogHeader} from '@astryxdesign/core/Dialog';
|
|
36
|
+
import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
|
|
37
|
+
import {MoreMenu} from '@astryxdesign/core/MoreMenu';
|
|
38
|
+
import {Toolbar} from '@astryxdesign/core/Toolbar';
|
|
39
|
+
import {useResizable, ResizeHandle} from '@astryxdesign/core/Resizable';
|
|
40
|
+
|
|
41
|
+
import {
|
|
42
|
+
FileText,
|
|
43
|
+
Copy,
|
|
44
|
+
Share2,
|
|
45
|
+
AtSign,
|
|
46
|
+
Paperclip,
|
|
47
|
+
X,
|
|
48
|
+
ChevronRight,
|
|
49
|
+
} from 'lucide-react';
|
|
50
|
+
|
|
51
|
+
// Below this container width the split-pane collapses to a single chat column
|
|
52
|
+
// and the artifact opens as a full-screen dialog instead. Shared by the CSS
|
|
53
|
+
// container query and the JS check in openArtifact so they can't drift. 1023
|
|
54
|
+
// keeps tablet widths from squeezing the chat into a ~100px rail beside the
|
|
55
|
+
// 640px default artifact panel.
|
|
56
|
+
const MOBILE_MAX_WIDTH = 1023;
|
|
57
|
+
|
|
58
|
+
const root: CSSProperties = {
|
|
59
|
+
// The showcase renders a 44px breadcrumb row above the template, so a full
|
|
60
|
+
// 100dvh root always spills exactly that far into page-level scroll. Subtract
|
|
61
|
+
// it so scrolling stays inside the message list and artifact body.
|
|
62
|
+
height: 'calc(100dvh - 44px)',
|
|
63
|
+
width: '100%',
|
|
64
|
+
containerType: 'inline-size',
|
|
65
|
+
containerName: 'artifact',
|
|
66
|
+
};
|
|
67
|
+
const chatColumn: CSSProperties = {
|
|
68
|
+
flex: 1,
|
|
69
|
+
width: '100%',
|
|
70
|
+
minWidth: 0,
|
|
71
|
+
height: '100%',
|
|
72
|
+
};
|
|
73
|
+
const chatLayout: CSSProperties = {
|
|
74
|
+
flex: 1,
|
|
75
|
+
minHeight: 0,
|
|
76
|
+
};
|
|
77
|
+
const artifactCard: CSSProperties = {
|
|
78
|
+
marginBlockStart: 'var(--space-gap)',
|
|
79
|
+
};
|
|
80
|
+
const artifactScroll: CSSProperties = {
|
|
81
|
+
flex: 1,
|
|
82
|
+
overflowY: 'auto',
|
|
83
|
+
};
|
|
84
|
+
const articleBody: CSSProperties = {
|
|
85
|
+
maxWidth: 720,
|
|
86
|
+
marginInline: 'auto',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Runtime width for the artifact panel, passed in via the --artifact-panel-width
|
|
90
|
+
// custom property so the MOBILE container query can still override it to 100%
|
|
91
|
+
// (an inline `width` would beat the class rule). The container query lives in a
|
|
92
|
+
// plain <style> tag below so it needs NO CSS compiler.
|
|
93
|
+
const artifactPanelWidthVar = (size: number | string): CSSProperties =>
|
|
94
|
+
({
|
|
95
|
+
'--artifact-panel-width': typeof size === 'number' ? `${size}px` : size,
|
|
96
|
+
}) as CSSProperties;
|
|
97
|
+
|
|
98
|
+
const AI_CHAT_CSS = `
|
|
99
|
+
.ai-chat-resize-handle {
|
|
100
|
+
display: flex;
|
|
101
|
+
}
|
|
102
|
+
.ai-chat-artifact-panel {
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
display: flex;
|
|
105
|
+
flex-direction: column;
|
|
106
|
+
width: var(--artifact-panel-width);
|
|
107
|
+
flex-shrink: 0;
|
|
108
|
+
}
|
|
109
|
+
@container artifact (max-width: ${MOBILE_MAX_WIDTH}px) {
|
|
110
|
+
.ai-chat-resize-handle {
|
|
111
|
+
display: none;
|
|
112
|
+
}
|
|
113
|
+
.ai-chat-artifact-panel {
|
|
114
|
+
display: none;
|
|
115
|
+
width: 100%;
|
|
116
|
+
flex-shrink: 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
// Artifact content
|
|
122
|
+
|
|
123
|
+
const MENTION_TOKENS = [
|
|
124
|
+
{value: '@agent', label: '@Agent', variant: 'blue' as const},
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const ARTIFACT_TITLE = 'JWT Token Refresh: Design & Rollout';
|
|
128
|
+
|
|
129
|
+
const ARTIFACT_SUBTITLE = 'Grimoire · Sealed just now';
|
|
130
|
+
|
|
131
|
+
const ARTIFACT_CONTENT = `## Overview
|
|
132
|
+
|
|
133
|
+
Our API gateway authenticates every request with a short-lived JWT access token. Until now, an expired token meant an immediate \`401\` — even when the user still held a valid refresh token. This document describes the silent-refresh flow we just shipped and how we're rolling it out.
|
|
134
|
+
|
|
135
|
+
## The Problem
|
|
136
|
+
|
|
137
|
+
Token validation ran **before** any refresh logic, so the middleware rejected expired tokens outright:
|
|
138
|
+
|
|
139
|
+
1. A request arrives with an expired access token
|
|
140
|
+
2. \`validateToken()\` throws \`TokenExpiredError\`
|
|
141
|
+
3. The catch block returns \`401\` — \`refreshToken()\` is never reached
|
|
142
|
+
|
|
143
|
+
The result was users getting logged out whenever an access token lapsed mid-session.
|
|
144
|
+
|
|
145
|
+
## The Fix
|
|
146
|
+
|
|
147
|
+
The middleware now catches \`TokenExpiredError\` specifically and attempts a silent refresh before rejecting. On success it reissues an access token and continues the request; on failure it falls back to \`401\`.
|
|
148
|
+
|
|
149
|
+
- **Transparent** — valid sessions never see an interruption
|
|
150
|
+
- **Safe** — a missing or invalid refresh token still returns \`401\`
|
|
151
|
+
- **Cheap** — refresh only runs on the expiry path, not on every request
|
|
152
|
+
|
|
153
|
+
## Testing
|
|
154
|
+
|
|
155
|
+
The refresh path is covered end to end:
|
|
156
|
+
|
|
157
|
+
| Scenario | Expected |
|
|
158
|
+
|----------|----------|
|
|
159
|
+
| Valid token passes through | \`200\` |
|
|
160
|
+
| Expired token, valid refresh | \`200\` + new access token |
|
|
161
|
+
| Expired token, invalid refresh | \`401\` |
|
|
162
|
+
| Malformed token | \`401\` |
|
|
163
|
+
|
|
164
|
+
## Rollout & Monitoring
|
|
165
|
+
|
|
166
|
+
1. Ship behind the \`silent_refresh\` flag at 5% of traffic
|
|
167
|
+
2. Watch the \`auth.refresh.success\` and \`auth.refresh.failure\` counters
|
|
168
|
+
3. Alert if the failure rate exceeds **2%** over any 5-minute window
|
|
169
|
+
4. Ramp to 100% once metrics hold steady for 24 hours`;
|
|
170
|
+
|
|
171
|
+
// Artifact subviews
|
|
172
|
+
|
|
173
|
+
// Header actions: version menu, copy, share. Pass `onClose` for the desktop
|
|
174
|
+
// close button. A fragment so each control is a direct child of the toolbar.
|
|
175
|
+
function ArtifactActions({onClose}: {onClose?: () => void}) {
|
|
176
|
+
return (
|
|
177
|
+
<>
|
|
178
|
+
<DropdownMenu
|
|
179
|
+
button={{
|
|
180
|
+
label: 'v2',
|
|
181
|
+
variant: 'ghost',
|
|
182
|
+
size: 'sm',
|
|
183
|
+
}}
|
|
184
|
+
items={[{label: 'v2 (current)'}, {label: 'v1'}]}
|
|
185
|
+
/>
|
|
186
|
+
<Button
|
|
187
|
+
label="Copy"
|
|
188
|
+
variant="ghost"
|
|
189
|
+
size="sm"
|
|
190
|
+
icon={<Icon icon={Copy} size="sm" />}
|
|
191
|
+
isIconOnly
|
|
192
|
+
/>
|
|
193
|
+
<Button
|
|
194
|
+
label="Share"
|
|
195
|
+
variant="ghost"
|
|
196
|
+
size="sm"
|
|
197
|
+
icon={<Icon icon={Share2} size="sm" />}
|
|
198
|
+
isIconOnly
|
|
199
|
+
/>
|
|
200
|
+
{onClose != null && (
|
|
201
|
+
<Button
|
|
202
|
+
label="Close document"
|
|
203
|
+
variant="ghost"
|
|
204
|
+
size="sm"
|
|
205
|
+
icon={<Icon icon={X} size="sm" />}
|
|
206
|
+
isIconOnly
|
|
207
|
+
onClick={onClose}
|
|
208
|
+
/>
|
|
209
|
+
)}
|
|
210
|
+
</>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Mobile variant: the actions collapse into an overflow menu.
|
|
215
|
+
function MobileArtifactActions() {
|
|
216
|
+
return (
|
|
217
|
+
<MoreMenu
|
|
218
|
+
label="Document actions"
|
|
219
|
+
size="sm"
|
|
220
|
+
items={[
|
|
221
|
+
{
|
|
222
|
+
type: 'section',
|
|
223
|
+
title: 'Version',
|
|
224
|
+
items: [
|
|
225
|
+
{label: 'v2 (current)', onClick: () => {}},
|
|
226
|
+
{label: 'v1', onClick: () => {}},
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
{type: 'divider'},
|
|
230
|
+
{label: 'Copy', icon: Copy},
|
|
231
|
+
{label: 'Share', icon: Share2},
|
|
232
|
+
]}
|
|
233
|
+
/>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Scrollable artifact body — the formatted document.
|
|
238
|
+
function ArtifactBody() {
|
|
239
|
+
return (
|
|
240
|
+
<Section variant="transparent" style={artifactScroll}>
|
|
241
|
+
<VStack gap={2} style={articleBody}>
|
|
242
|
+
<Heading level={1}>{ARTIFACT_TITLE}</Heading>
|
|
243
|
+
<Markdown>{ARTIFACT_CONTENT}</Markdown>
|
|
244
|
+
</VStack>
|
|
245
|
+
</Section>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// In-message card that opens the artifact panel/dialog.
|
|
250
|
+
function ArtifactCard({onOpen}: {onOpen: () => void}) {
|
|
251
|
+
return (
|
|
252
|
+
<ClickableCard
|
|
253
|
+
label={`Open ${ARTIFACT_TITLE}`}
|
|
254
|
+
onClick={onOpen}
|
|
255
|
+
variant="muted"
|
|
256
|
+
padding={3}
|
|
257
|
+
maxWidth={360}
|
|
258
|
+
style={artifactCard}>
|
|
259
|
+
<HStack gap={3} vAlign="center" width="100%">
|
|
260
|
+
<Icon icon={FileText} size="md" color="secondary" />
|
|
261
|
+
<StackItem size="fill">
|
|
262
|
+
<VStack gap={0}>
|
|
263
|
+
<Text type="label" weight="semibold">
|
|
264
|
+
{ARTIFACT_TITLE}
|
|
265
|
+
</Text>
|
|
266
|
+
<Text type="supporting" color="secondary">
|
|
267
|
+
Grimoire
|
|
268
|
+
</Text>
|
|
269
|
+
</VStack>
|
|
270
|
+
</StackItem>
|
|
271
|
+
<Icon icon={ChevronRight} size="sm" color="secondary" />
|
|
272
|
+
</HStack>
|
|
273
|
+
</ClickableCard>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Main component
|
|
278
|
+
|
|
279
|
+
export default function AiChat() {
|
|
280
|
+
const [composerMode, setComposerMode] = useState('ask');
|
|
281
|
+
// Mobile shows the artifact as a full-screen dialog; desktop as a side panel.
|
|
282
|
+
const [isArtifactDialogOpen, setIsArtifactDialogOpen] = useState(false);
|
|
283
|
+
const [isArtifactOpen, setIsArtifactOpen] = useState(true);
|
|
284
|
+
const rootRef = useRef<HTMLElement>(null);
|
|
285
|
+
const artifactResize = useResizable({
|
|
286
|
+
defaultSize: 640,
|
|
287
|
+
minSizePx: 480,
|
|
288
|
+
maxSizePx: 960,
|
|
289
|
+
autoSaveId: 'ai-chat-artifact-panel',
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Match the container-query breakpoint by measuring the root, not the viewport.
|
|
293
|
+
const openArtifact = () => {
|
|
294
|
+
const width = rootRef.current?.offsetWidth ?? Infinity;
|
|
295
|
+
if (width <= MOBILE_MAX_WIDTH) {
|
|
296
|
+
setIsArtifactDialogOpen(true);
|
|
297
|
+
} else {
|
|
298
|
+
setIsArtifactOpen(true);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<VStack ref={rootRef} style={root}>
|
|
304
|
+
<style>{AI_CHAT_CSS}</style>
|
|
305
|
+
<Layout
|
|
306
|
+
height="fill"
|
|
307
|
+
content={
|
|
308
|
+
<LayoutContent padding={0}>
|
|
309
|
+
<HStack height="100%">
|
|
310
|
+
{/* Chat column — flexes to fill the space the artifact leaves */}
|
|
311
|
+
<VStack style={chatColumn}>
|
|
312
|
+
<ChatLayout
|
|
313
|
+
density="spacious"
|
|
314
|
+
style={chatLayout}
|
|
315
|
+
composer={
|
|
316
|
+
<ChatComposer
|
|
317
|
+
onSubmit={() => {}}
|
|
318
|
+
placeholder={
|
|
319
|
+
composerMode === 'ask'
|
|
320
|
+
? 'Ask the night anything...'
|
|
321
|
+
: 'Describe the rite...'
|
|
322
|
+
}
|
|
323
|
+
input={<ChatComposerInput />}
|
|
324
|
+
headerActions={
|
|
325
|
+
<>
|
|
326
|
+
<Button
|
|
327
|
+
label="Mention"
|
|
328
|
+
variant="ghost"
|
|
329
|
+
size="sm"
|
|
330
|
+
icon={<Icon icon={AtSign} size="sm" />}
|
|
331
|
+
isIconOnly
|
|
332
|
+
/>
|
|
333
|
+
<Button
|
|
334
|
+
label="Attach"
|
|
335
|
+
variant="ghost"
|
|
336
|
+
size="sm"
|
|
337
|
+
icon={<Icon icon={Paperclip} size="sm" />}
|
|
338
|
+
isIconOnly
|
|
339
|
+
/>
|
|
340
|
+
</>
|
|
341
|
+
}
|
|
342
|
+
footerActions={
|
|
343
|
+
<DropdownMenu
|
|
344
|
+
button={{
|
|
345
|
+
label: composerMode === 'ask' ? 'Ask' : 'Edit',
|
|
346
|
+
variant: 'ghost',
|
|
347
|
+
size: 'sm',
|
|
348
|
+
}}
|
|
349
|
+
items={[
|
|
350
|
+
{
|
|
351
|
+
label: 'Ask',
|
|
352
|
+
onClick: () => setComposerMode('ask'),
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
label: 'Edit',
|
|
356
|
+
onClick: () => setComposerMode('edit'),
|
|
357
|
+
},
|
|
358
|
+
]}
|
|
359
|
+
/>
|
|
360
|
+
}
|
|
361
|
+
/>
|
|
362
|
+
}>
|
|
363
|
+
<ChatMessageList>
|
|
364
|
+
{/* Date divider */}
|
|
365
|
+
<ChatSystemMessage variant="divider">
|
|
366
|
+
Today
|
|
367
|
+
</ChatSystemMessage>
|
|
368
|
+
|
|
369
|
+
{/* User message: mention + file attachments */}
|
|
370
|
+
<ChatMessage sender="user">
|
|
371
|
+
<HStack gap={1} wrap="wrap">
|
|
372
|
+
<Token label="auth-service.ts" />
|
|
373
|
+
<Token label="middleware.ts" />
|
|
374
|
+
</HStack>
|
|
375
|
+
<ChatMessageBubble
|
|
376
|
+
metadata={
|
|
377
|
+
<ChatMessageMetadata
|
|
378
|
+
timestamp={
|
|
379
|
+
<Timestamp
|
|
380
|
+
value="2026-04-29T10:15:00"
|
|
381
|
+
format="time"
|
|
382
|
+
/>
|
|
383
|
+
}
|
|
384
|
+
/>
|
|
385
|
+
}>
|
|
386
|
+
<ChatTokenizedText tokens={MENTION_TOKENS}>
|
|
387
|
+
@agent Can you review these auth files? The JWT
|
|
388
|
+
refresh logic seems broken — tokens expire but the
|
|
389
|
+
middleware doesn't catch it.
|
|
390
|
+
</ChatTokenizedText>
|
|
391
|
+
</ChatMessageBubble>
|
|
392
|
+
</ChatMessage>
|
|
393
|
+
|
|
394
|
+
{/* Assistant message: tool calls, markdown, code block */}
|
|
395
|
+
<ChatMessage
|
|
396
|
+
sender="assistant"
|
|
397
|
+
avatar={<Avatar name="Agent" size="md" />}>
|
|
398
|
+
<ChatMessageBubble variant="ghost">
|
|
399
|
+
Looking into the auth files now. Let me read through the
|
|
400
|
+
code and trace the token refresh flow.
|
|
401
|
+
</ChatMessageBubble>
|
|
402
|
+
<ChatToolCalls
|
|
403
|
+
defaultIsExpanded
|
|
404
|
+
calls={[
|
|
405
|
+
{
|
|
406
|
+
name: 'read',
|
|
407
|
+
target: 'auth-service.ts',
|
|
408
|
+
status: 'complete',
|
|
409
|
+
duration: '45ms',
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'read',
|
|
413
|
+
target: 'middleware.ts',
|
|
414
|
+
status: 'complete',
|
|
415
|
+
duration: '38ms',
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: 'bash',
|
|
419
|
+
target: 'grep -rn "refreshToken" src/',
|
|
420
|
+
status: 'complete',
|
|
421
|
+
duration: '120ms',
|
|
422
|
+
node: 'cli:remote-server',
|
|
423
|
+
},
|
|
424
|
+
]}
|
|
425
|
+
/>
|
|
426
|
+
|
|
427
|
+
<ChatMessageBubble variant="ghost">
|
|
428
|
+
<Markdown density="compact">{`Found the issue. In \`middleware.ts\`, the token validation runs **before** the refresh check. When a token expires, the middleware rejects the request immediately instead of attempting a refresh.
|
|
429
|
+
|
|
430
|
+
Here's the problematic sequence:
|
|
431
|
+
|
|
432
|
+
1. Request arrives with an expired access token
|
|
433
|
+
2. \`validateToken()\` throws \`TokenExpiredError\`
|
|
434
|
+
3. The catch block returns \`401\` — never reaching \`refreshToken()\`
|
|
435
|
+
|
|
436
|
+
The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh before rejecting:`}</Markdown>
|
|
437
|
+
</ChatMessageBubble>
|
|
438
|
+
|
|
439
|
+
<ChatMessageBubble variant="ghost">
|
|
440
|
+
<CodeBlock
|
|
441
|
+
title="middleware.ts"
|
|
442
|
+
language="typescript"
|
|
443
|
+
code={`async function authMiddleware(req: Request) {
|
|
444
|
+
try {
|
|
445
|
+
const decoded = validateToken(req.headers.authorization);
|
|
446
|
+
req.user = decoded;
|
|
447
|
+
} catch (err) {
|
|
448
|
+
if (err instanceof TokenExpiredError) {
|
|
449
|
+
// Attempt silent refresh before rejecting
|
|
450
|
+
const refreshed = await refreshToken(req.cookies.refreshToken);
|
|
451
|
+
if (refreshed) {
|
|
452
|
+
req.user = refreshed.user;
|
|
453
|
+
req.newAccessToken = refreshed.accessToken;
|
|
454
|
+
return next(req);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return new Response('Unauthorized', { status: 401 });
|
|
458
|
+
}
|
|
459
|
+
return next(req);
|
|
460
|
+
}`}
|
|
461
|
+
/>
|
|
462
|
+
</ChatMessageBubble>
|
|
463
|
+
|
|
464
|
+
<ChatToolCalls
|
|
465
|
+
calls={[
|
|
466
|
+
{
|
|
467
|
+
name: 'edit',
|
|
468
|
+
target: 'middleware.ts',
|
|
469
|
+
status: 'complete',
|
|
470
|
+
duration: '85ms',
|
|
471
|
+
additions: 8,
|
|
472
|
+
deletions: 2,
|
|
473
|
+
},
|
|
474
|
+
]}
|
|
475
|
+
/>
|
|
476
|
+
|
|
477
|
+
<ChatMessageMetadata
|
|
478
|
+
timestamp={
|
|
479
|
+
<Timestamp
|
|
480
|
+
value="2026-04-29T10:15:30"
|
|
481
|
+
format="time"
|
|
482
|
+
/>
|
|
483
|
+
}
|
|
484
|
+
footer={
|
|
485
|
+
<Text type="supporting" color="secondary">
|
|
486
|
+
Agent
|
|
487
|
+
</Text>
|
|
488
|
+
}
|
|
489
|
+
/>
|
|
490
|
+
</ChatMessage>
|
|
491
|
+
|
|
492
|
+
{/* User message: multi-bubble grouping */}
|
|
493
|
+
<ChatMessage sender="user">
|
|
494
|
+
<ChatMessageBubble group="first">
|
|
495
|
+
Nice catch, that makes sense
|
|
496
|
+
</ChatMessageBubble>
|
|
497
|
+
<ChatMessageBubble
|
|
498
|
+
group="last"
|
|
499
|
+
metadata={
|
|
500
|
+
<ChatMessageMetadata
|
|
501
|
+
timestamp={
|
|
502
|
+
<Timestamp
|
|
503
|
+
value="2026-04-29T10:16:00"
|
|
504
|
+
format="time"
|
|
505
|
+
/>
|
|
506
|
+
}
|
|
507
|
+
status="delivered"
|
|
508
|
+
/>
|
|
509
|
+
}>
|
|
510
|
+
Can you also add a test for the refresh path?
|
|
511
|
+
</ChatMessageBubble>
|
|
512
|
+
</ChatMessage>
|
|
513
|
+
|
|
514
|
+
{/* Assistant message: test results table + code block */}
|
|
515
|
+
<ChatMessage
|
|
516
|
+
sender="assistant"
|
|
517
|
+
avatar={<Avatar name="Agent" size="md" />}>
|
|
518
|
+
<ChatToolCalls
|
|
519
|
+
defaultIsExpanded
|
|
520
|
+
calls={[
|
|
521
|
+
{
|
|
522
|
+
name: 'read',
|
|
523
|
+
target: 'middleware.test.ts',
|
|
524
|
+
status: 'complete',
|
|
525
|
+
duration: '32ms',
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
name: 'edit',
|
|
529
|
+
target: 'middleware.test.ts',
|
|
530
|
+
status: 'complete',
|
|
531
|
+
duration: '110ms',
|
|
532
|
+
additions: 24,
|
|
533
|
+
deletions: 0,
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: 'bash',
|
|
537
|
+
target: 'yarn test middleware',
|
|
538
|
+
status: 'complete',
|
|
539
|
+
duration: '3.2s',
|
|
540
|
+
node: 'cli:remote-server',
|
|
541
|
+
},
|
|
542
|
+
]}
|
|
543
|
+
/>
|
|
544
|
+
<ChatMessageBubble variant="ghost">
|
|
545
|
+
<Markdown density="compact">{`Added a test for the refresh flow. All **4 tests** pass:
|
|
546
|
+
|
|
547
|
+
| Test | Status |
|
|
548
|
+
|------|--------|
|
|
549
|
+
| Valid token passes through | ✅ |
|
|
550
|
+
| Expired token triggers refresh | ✅ |
|
|
551
|
+
| Expired token with invalid refresh returns 401 | ✅ |
|
|
552
|
+
| Malformed token returns 401 immediately | ✅ |`}</Markdown>
|
|
553
|
+
</ChatMessageBubble>
|
|
554
|
+
|
|
555
|
+
<ChatMessageBubble variant="ghost">
|
|
556
|
+
<CodeBlock
|
|
557
|
+
title="middleware.test.ts"
|
|
558
|
+
language="typescript"
|
|
559
|
+
code={`describe('authMiddleware', () => {
|
|
560
|
+
it('refreshes an expired token silently', async () => {
|
|
561
|
+
const expiredToken = createExpiredJWT(mockUser);
|
|
562
|
+
const validRefresh = createRefreshToken(mockUser);
|
|
563
|
+
|
|
564
|
+
const req = mockRequest({
|
|
565
|
+
authorization: \`Bearer \${expiredToken}\`,
|
|
566
|
+
cookies: { refreshToken: validRefresh },
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const res = await authMiddleware(req);
|
|
570
|
+
|
|
571
|
+
expect(res.status).toBe(200);
|
|
572
|
+
expect(req.user.id).toBe(mockUser.id);
|
|
573
|
+
expect(req.newAccessToken).toBeDefined();
|
|
574
|
+
});
|
|
575
|
+
});`}
|
|
576
|
+
/>
|
|
577
|
+
</ChatMessageBubble>
|
|
578
|
+
<ChatMessageMetadata
|
|
579
|
+
timestamp={
|
|
580
|
+
<Timestamp
|
|
581
|
+
value="2026-04-29T10:16:45"
|
|
582
|
+
format="time"
|
|
583
|
+
/>
|
|
584
|
+
}
|
|
585
|
+
/>
|
|
586
|
+
</ChatMessage>
|
|
587
|
+
|
|
588
|
+
{/* Status message */}
|
|
589
|
+
<ChatSystemMessage>
|
|
590
|
+
Changes sealed in the crypt
|
|
591
|
+
</ChatSystemMessage>
|
|
592
|
+
|
|
593
|
+
{/* User message: requests a document artifact */}
|
|
594
|
+
<ChatMessage sender="user">
|
|
595
|
+
<ChatMessageBubble
|
|
596
|
+
metadata={
|
|
597
|
+
<ChatMessageMetadata
|
|
598
|
+
timestamp={
|
|
599
|
+
<Timestamp
|
|
600
|
+
value="2026-04-29T10:18:00"
|
|
601
|
+
format="time"
|
|
602
|
+
/>
|
|
603
|
+
}
|
|
604
|
+
/>
|
|
605
|
+
}>
|
|
606
|
+
Looks solid. Before I open the PR, can you write up a
|
|
607
|
+
short design doc explaining the token-refresh flow for
|
|
608
|
+
the team?
|
|
609
|
+
</ChatMessageBubble>
|
|
610
|
+
</ChatMessage>
|
|
611
|
+
|
|
612
|
+
{/* Assistant message: artifact card */}
|
|
613
|
+
<ChatMessage
|
|
614
|
+
sender="assistant"
|
|
615
|
+
avatar={<Avatar name="Agent" size="md" />}>
|
|
616
|
+
<ChatMessageBubble variant="ghost">
|
|
617
|
+
<Markdown density="compact">
|
|
618
|
+
{`I've drafted a design doc covering the problem, the fix, and the test matrix — pulling straight from the changes we just made.\n\nOpen the document below to review it. Want me to expand any section?`}
|
|
619
|
+
</Markdown>
|
|
620
|
+
</ChatMessageBubble>
|
|
621
|
+
<ArtifactCard onOpen={openArtifact} />
|
|
622
|
+
<ChatMessageMetadata
|
|
623
|
+
timestamp={
|
|
624
|
+
<Timestamp
|
|
625
|
+
value="2026-04-29T10:18:40"
|
|
626
|
+
format="time"
|
|
627
|
+
/>
|
|
628
|
+
}
|
|
629
|
+
/>
|
|
630
|
+
</ChatMessage>
|
|
631
|
+
|
|
632
|
+
{/* User follow-up */}
|
|
633
|
+
<ChatMessage sender="user">
|
|
634
|
+
<ChatMessageBubble
|
|
635
|
+
metadata={
|
|
636
|
+
<ChatMessageMetadata
|
|
637
|
+
timestamp={
|
|
638
|
+
<Timestamp
|
|
639
|
+
value="2026-04-29T10:20:00"
|
|
640
|
+
format="time"
|
|
641
|
+
/>
|
|
642
|
+
}
|
|
643
|
+
status="delivered"
|
|
644
|
+
/>
|
|
645
|
+
}>
|
|
646
|
+
This is great. Can you add a section on rollout and
|
|
647
|
+
monitoring at the end?
|
|
648
|
+
</ChatMessageBubble>
|
|
649
|
+
</ChatMessage>
|
|
650
|
+
|
|
651
|
+
{/* Assistant message: in-progress tool call */}
|
|
652
|
+
<ChatMessage
|
|
653
|
+
sender="assistant"
|
|
654
|
+
avatar={<Avatar name="Agent" size="md" />}>
|
|
655
|
+
<ChatMessageBubble variant="ghost">
|
|
656
|
+
<Markdown density="compact">
|
|
657
|
+
{`On it — adding a **Rollout & Monitoring** section with a staged flag ramp and the alert thresholds. Updating the document now.`}
|
|
658
|
+
</Markdown>
|
|
659
|
+
</ChatMessageBubble>
|
|
660
|
+
<ChatToolCalls
|
|
661
|
+
calls={[
|
|
662
|
+
{
|
|
663
|
+
name: 'edit',
|
|
664
|
+
target: 'docs/token-refresh.md',
|
|
665
|
+
status: 'running',
|
|
666
|
+
node: 'cli:remote-server',
|
|
667
|
+
},
|
|
668
|
+
]}
|
|
669
|
+
/>
|
|
670
|
+
<ChatMessageMetadata
|
|
671
|
+
timestamp={
|
|
672
|
+
<Timestamp
|
|
673
|
+
value="2026-04-29T10:20:30"
|
|
674
|
+
format="time"
|
|
675
|
+
/>
|
|
676
|
+
}
|
|
677
|
+
/>
|
|
678
|
+
</ChatMessage>
|
|
679
|
+
</ChatMessageList>
|
|
680
|
+
</ChatLayout>
|
|
681
|
+
</VStack>
|
|
682
|
+
|
|
683
|
+
{/* Desktop split-pane: resize handle + artifact panel */}
|
|
684
|
+
{isArtifactOpen && (
|
|
685
|
+
<>
|
|
686
|
+
<ResizeHandle
|
|
687
|
+
direction="horizontal"
|
|
688
|
+
resizable={artifactResize.props}
|
|
689
|
+
isReversed
|
|
690
|
+
pillPlacement="start"
|
|
691
|
+
hasDivider
|
|
692
|
+
label="Resize artifact panel"
|
|
693
|
+
className="ai-chat-resize-handle"
|
|
694
|
+
/>
|
|
695
|
+
|
|
696
|
+
{/* Toolbar as the card header, body below */}
|
|
697
|
+
<Card
|
|
698
|
+
variant="transparent"
|
|
699
|
+
height="100%"
|
|
700
|
+
className="ai-chat-artifact-panel"
|
|
701
|
+
style={artifactPanelWidthVar(artifactResize.size)}>
|
|
702
|
+
<Toolbar
|
|
703
|
+
label="Artifact actions"
|
|
704
|
+
dividers={['bottom']}
|
|
705
|
+
startContent={
|
|
706
|
+
<HStack gap={3} vAlign="center">
|
|
707
|
+
<Icon
|
|
708
|
+
icon={FileText}
|
|
709
|
+
size="sm"
|
|
710
|
+
color="secondary"
|
|
711
|
+
/>
|
|
712
|
+
<VStack gap={0}>
|
|
713
|
+
<Text type="label" weight="semibold">
|
|
714
|
+
{ARTIFACT_TITLE}
|
|
715
|
+
</Text>
|
|
716
|
+
<Text type="supporting" color="secondary">
|
|
717
|
+
{ARTIFACT_SUBTITLE}
|
|
718
|
+
</Text>
|
|
719
|
+
</VStack>
|
|
720
|
+
</HStack>
|
|
721
|
+
}
|
|
722
|
+
endContent={
|
|
723
|
+
<ArtifactActions
|
|
724
|
+
onClose={() => setIsArtifactOpen(false)}
|
|
725
|
+
/>
|
|
726
|
+
}
|
|
727
|
+
/>
|
|
728
|
+
|
|
729
|
+
<ArtifactBody />
|
|
730
|
+
</Card>
|
|
731
|
+
</>
|
|
732
|
+
)}
|
|
733
|
+
</HStack>
|
|
734
|
+
</LayoutContent>
|
|
735
|
+
}
|
|
736
|
+
/>
|
|
737
|
+
{/* Mobile artifact view — full-screen Dialog */}
|
|
738
|
+
<Dialog
|
|
739
|
+
isOpen={isArtifactDialogOpen}
|
|
740
|
+
onOpenChange={setIsArtifactDialogOpen}
|
|
741
|
+
purpose="info"
|
|
742
|
+
variant="fullscreen">
|
|
743
|
+
<Layout
|
|
744
|
+
header={
|
|
745
|
+
<DialogHeader
|
|
746
|
+
title={ARTIFACT_TITLE}
|
|
747
|
+
subtitle={ARTIFACT_SUBTITLE}
|
|
748
|
+
hasDivider
|
|
749
|
+
onOpenChange={setIsArtifactDialogOpen}
|
|
750
|
+
endContent={<MobileArtifactActions />}
|
|
751
|
+
/>
|
|
752
|
+
}
|
|
753
|
+
content={
|
|
754
|
+
<LayoutContent padding={0}>
|
|
755
|
+
<ArtifactBody />
|
|
756
|
+
</LayoutContent>
|
|
757
|
+
}
|
|
758
|
+
/>
|
|
759
|
+
</Dialog>
|
|
760
|
+
</VStack>
|
|
761
|
+
);
|
|
762
|
+
}
|