@schlessera/brain-ui-react 0.4.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 +21 -0
- package/README.md +70 -0
- package/dist/components/chat/ask-user-card.d.ts +12 -0
- package/dist/components/chat/ask-user-card.d.ts.map +1 -0
- package/dist/components/chat/ask-user-card.js +185 -0
- package/dist/components/chat/ask-user-card.js.map +1 -0
- package/dist/components/chat/brain-markdown.d.ts +35 -0
- package/dist/components/chat/brain-markdown.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown.js +363 -0
- package/dist/components/chat/brain-markdown.js.map +1 -0
- package/dist/components/chat/chat-page.d.ts +2 -0
- package/dist/components/chat/chat-page.d.ts.map +1 -0
- package/dist/components/chat/chat-page.js +442 -0
- package/dist/components/chat/chat-page.js.map +1 -0
- package/dist/components/chat/command-palette.d.ts +5 -0
- package/dist/components/chat/command-palette.d.ts.map +1 -0
- package/dist/components/chat/command-palette.js +18 -0
- package/dist/components/chat/command-palette.js.map +1 -0
- package/dist/components/chat/markdown-content.d.ts +4 -0
- package/dist/components/chat/markdown-content.d.ts.map +1 -0
- package/dist/components/chat/markdown-content.js +6 -0
- package/dist/components/chat/markdown-content.js.map +1 -0
- package/dist/components/chat/message-bubble.d.ts +9 -0
- package/dist/components/chat/message-bubble.d.ts.map +1 -0
- package/dist/components/chat/message-bubble.js +129 -0
- package/dist/components/chat/message-bubble.js.map +1 -0
- package/dist/components/chat/message-share.d.ts +10 -0
- package/dist/components/chat/message-share.d.ts.map +1 -0
- package/dist/components/chat/message-share.js +65 -0
- package/dist/components/chat/message-share.js.map +1 -0
- package/dist/components/chat/renderers/claude-tools.d.ts +3 -0
- package/dist/components/chat/renderers/claude-tools.d.ts.map +1 -0
- package/dist/components/chat/renderers/claude-tools.js +38 -0
- package/dist/components/chat/renderers/claude-tools.js.map +1 -0
- package/dist/components/chat/renderers/generic.d.ts +9 -0
- package/dist/components/chat/renderers/generic.d.ts.map +1 -0
- package/dist/components/chat/renderers/generic.js +68 -0
- package/dist/components/chat/renderers/generic.js.map +1 -0
- package/dist/components/chat/renderers/index.d.ts +5 -0
- package/dist/components/chat/renderers/index.d.ts.map +1 -0
- package/dist/components/chat/renderers/index.js +17 -0
- package/dist/components/chat/renderers/index.js.map +1 -0
- package/dist/components/chat/risk-hints.d.ts +20 -0
- package/dist/components/chat/risk-hints.d.ts.map +1 -0
- package/dist/components/chat/risk-hints.js +81 -0
- package/dist/components/chat/risk-hints.js.map +1 -0
- package/dist/components/chat/session-drawer.d.ts +6 -0
- package/dist/components/chat/session-drawer.d.ts.map +1 -0
- package/dist/components/chat/session-drawer.js +96 -0
- package/dist/components/chat/session-drawer.js.map +1 -0
- package/dist/components/chat/share-block.d.ts +9 -0
- package/dist/components/chat/share-block.d.ts.map +1 -0
- package/dist/components/chat/share-block.js +70 -0
- package/dist/components/chat/share-block.js.map +1 -0
- package/dist/components/chat/tool-call-timeline.d.ts +8 -0
- package/dist/components/chat/tool-call-timeline.d.ts.map +1 -0
- package/dist/components/chat/tool-call-timeline.js +100 -0
- package/dist/components/chat/tool-call-timeline.js.map +1 -0
- package/dist/components/chat/tool-views.d.ts +116 -0
- package/dist/components/chat/tool-views.d.ts.map +1 -0
- package/dist/components/chat/tool-views.js +628 -0
- package/dist/components/chat/tool-views.js.map +1 -0
- package/dist/components/chat/welcome-state.d.ts +4 -0
- package/dist/components/chat/welcome-state.d.ts.map +1 -0
- package/dist/components/chat/welcome-state.js +10 -0
- package/dist/components/chat/welcome-state.js.map +1 -0
- package/dist/components/connectivity/connection-gate.d.ts +5 -0
- package/dist/components/connectivity/connection-gate.d.ts.map +1 -0
- package/dist/components/connectivity/connection-gate.js +42 -0
- package/dist/components/connectivity/connection-gate.js.map +1 -0
- package/dist/components/connectivity/login-screen.d.ts +9 -0
- package/dist/components/connectivity/login-screen.d.ts.map +1 -0
- package/dist/components/connectivity/login-screen.js +99 -0
- package/dist/components/connectivity/login-screen.js.map +1 -0
- package/dist/components/files/file-panel.d.ts +5 -0
- package/dist/components/files/file-panel.d.ts.map +1 -0
- package/dist/components/files/file-panel.js +46 -0
- package/dist/components/files/file-panel.js.map +1 -0
- package/dist/components/files/file-tree.d.ts +2 -0
- package/dist/components/files/file-tree.d.ts.map +1 -0
- package/dist/components/files/file-tree.js +63 -0
- package/dist/components/files/file-tree.js.map +1 -0
- package/dist/components/files/file-viewer-binary.d.ts +5 -0
- package/dist/components/files/file-viewer-binary.d.ts.map +1 -0
- package/dist/components/files/file-viewer-binary.js +20 -0
- package/dist/components/files/file-viewer-binary.js.map +1 -0
- package/dist/components/files/file-viewer-html.d.ts +4 -0
- package/dist/components/files/file-viewer-html.d.ts.map +1 -0
- package/dist/components/files/file-viewer-html.js +5 -0
- package/dist/components/files/file-viewer-html.js.map +1 -0
- package/dist/components/files/file-viewer-markdown.d.ts +4 -0
- package/dist/components/files/file-viewer-markdown.d.ts.map +1 -0
- package/dist/components/files/file-viewer-markdown.js +10 -0
- package/dist/components/files/file-viewer-markdown.js.map +1 -0
- package/dist/components/files/file-viewer-raw.d.ts +5 -0
- package/dist/components/files/file-viewer-raw.d.ts.map +1 -0
- package/dist/components/files/file-viewer-raw.js +15 -0
- package/dist/components/files/file-viewer-raw.js.map +1 -0
- package/dist/components/files/file-viewer.d.ts +2 -0
- package/dist/components/files/file-viewer.d.ts.map +1 -0
- package/dist/components/files/file-viewer.js +175 -0
- package/dist/components/files/file-viewer.js.map +1 -0
- package/dist/components/files/frontmatter-panel.d.ts +5 -0
- package/dist/components/files/frontmatter-panel.d.ts.map +1 -0
- package/dist/components/files/frontmatter-panel.js +12 -0
- package/dist/components/files/frontmatter-panel.js.map +1 -0
- package/dist/components/layout/app-shell.d.ts +5 -0
- package/dist/components/layout/app-shell.d.ts.map +1 -0
- package/dist/components/layout/app-shell.js +10 -0
- package/dist/components/layout/app-shell.js.map +1 -0
- package/dist/components/layout/mobile-tab-bar.d.ts +2 -0
- package/dist/components/layout/mobile-tab-bar.d.ts.map +1 -0
- package/dist/components/layout/mobile-tab-bar.js +16 -0
- package/dist/components/layout/mobile-tab-bar.js.map +1 -0
- package/dist/components/layout/side-rail.d.ts +2 -0
- package/dist/components/layout/side-rail.d.ts.map +1 -0
- package/dist/components/layout/side-rail.js +26 -0
- package/dist/components/layout/side-rail.js.map +1 -0
- package/dist/components/layout/slide-panel.d.ts +9 -0
- package/dist/components/layout/slide-panel.d.ts.map +1 -0
- package/dist/components/layout/slide-panel.js +18 -0
- package/dist/components/layout/slide-panel.js.map +1 -0
- package/dist/components/quick-actions/streaming-modal.d.ts +8 -0
- package/dist/components/quick-actions/streaming-modal.d.ts.map +1 -0
- package/dist/components/quick-actions/streaming-modal.js +92 -0
- package/dist/components/quick-actions/streaming-modal.js.map +1 -0
- package/dist/components/quick-actions/whatsup-modal.d.ts +5 -0
- package/dist/components/quick-actions/whatsup-modal.d.ts.map +1 -0
- package/dist/components/quick-actions/whatsup-modal.js +82 -0
- package/dist/components/quick-actions/whatsup-modal.js.map +1 -0
- package/dist/components/settings/passkey-panel.d.ts +10 -0
- package/dist/components/settings/passkey-panel.d.ts.map +1 -0
- package/dist/components/settings/passkey-panel.js +118 -0
- package/dist/components/settings/passkey-panel.js.map +1 -0
- package/dist/components/share/share-menu.d.ts +23 -0
- package/dist/components/share/share-menu.d.ts.map +1 -0
- package/dist/components/share/share-menu.js +58 -0
- package/dist/components/share/share-menu.js.map +1 -0
- package/dist/components/voice/dictation-sheet.d.ts +6 -0
- package/dist/components/voice/dictation-sheet.d.ts.map +1 -0
- package/dist/components/voice/dictation-sheet.js +91 -0
- package/dist/components/voice/dictation-sheet.js.map +1 -0
- package/dist/components/voice/mic-button.d.ts +6 -0
- package/dist/components/voice/mic-button.d.ts.map +1 -0
- package/dist/components/voice/mic-button.js +9 -0
- package/dist/components/voice/mic-button.js.map +1 -0
- package/dist/components/voice/review-card.d.ts +8 -0
- package/dist/components/voice/review-card.d.ts.map +1 -0
- package/dist/components/voice/review-card.js +8 -0
- package/dist/components/voice/review-card.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +10 -0
- package/dist/config.js.map +1 -0
- package/dist/hooks/use-vpn-status.d.ts +2 -0
- package/dist/hooks/use-vpn-status.d.ts.map +1 -0
- package/dist/hooks/use-vpn-status.js +71 -0
- package/dist/hooks/use-vpn-status.js.map +1 -0
- package/dist/hooks/use-websocket.d.ts +8 -0
- package/dist/hooks/use-websocket.d.ts.map +1 -0
- package/dist/hooks/use-websocket.js +366 -0
- package/dist/hooks/use-websocket.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-client.d.ts +115 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +70 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/backend.d.ts +7 -0
- package/dist/lib/backend.d.ts.map +1 -0
- package/dist/lib/backend.js +33 -0
- package/dist/lib/backend.js.map +1 -0
- package/dist/lib/frontmatter.d.ts +21 -0
- package/dist/lib/frontmatter.d.ts.map +1 -0
- package/dist/lib/frontmatter.js +88 -0
- package/dist/lib/frontmatter.js.map +1 -0
- package/dist/lib/image-attachments.d.ts +36 -0
- package/dist/lib/image-attachments.d.ts.map +1 -0
- package/dist/lib/image-attachments.js +175 -0
- package/dist/lib/image-attachments.js.map +1 -0
- package/dist/lib/passkeys.d.ts +18 -0
- package/dist/lib/passkeys.d.ts.map +1 -0
- package/dist/lib/passkeys.js +36 -0
- package/dist/lib/passkeys.js.map +1 -0
- package/dist/lib/share.d.ts +53 -0
- package/dist/lib/share.d.ts.map +1 -0
- package/dist/lib/share.js +144 -0
- package/dist/lib/share.js.map +1 -0
- package/dist/lib/strip-markdown.d.ts +10 -0
- package/dist/lib/strip-markdown.d.ts.map +1 -0
- package/dist/lib/strip-markdown.js +37 -0
- package/dist/lib/strip-markdown.js.map +1 -0
- package/dist/lib/tool-names.d.ts +17 -0
- package/dist/lib/tool-names.d.ts.map +1 -0
- package/dist/lib/tool-names.js +23 -0
- package/dist/lib/tool-names.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/lib/ws-client.d.ts +26 -0
- package/dist/lib/ws-client.d.ts.map +1 -0
- package/dist/lib/ws-client.js +93 -0
- package/dist/lib/ws-client.js.map +1 -0
- package/dist/stores/chat-store.d.ts +131 -0
- package/dist/stores/chat-store.d.ts.map +1 -0
- package/dist/stores/chat-store.js +376 -0
- package/dist/stores/chat-store.js.map +1 -0
- package/dist/stores/connection-store.d.ts +11 -0
- package/dist/stores/connection-store.d.ts.map +1 -0
- package/dist/stores/connection-store.js +8 -0
- package/dist/stores/connection-store.js.map +1 -0
- package/dist/stores/file-store.d.ts +53 -0
- package/dist/stores/file-store.d.ts.map +1 -0
- package/dist/stores/file-store.js +284 -0
- package/dist/stores/file-store.js.map +1 -0
- package/dist/stores/provider-store.d.ts +24 -0
- package/dist/stores/provider-store.d.ts.map +1 -0
- package/dist/stores/provider-store.js +56 -0
- package/dist/stores/provider-store.js.map +1 -0
- package/dist/stores/ui-store.d.ts +21 -0
- package/dist/stores/ui-store.d.ts.map +1 -0
- package/dist/stores/ui-store.js +23 -0
- package/dist/stores/ui-store.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/voice/asr-clients.d.ts +2 -0
- package/dist/voice/asr-clients.d.ts.map +1 -0
- package/dist/voice/asr-clients.js +33 -0
- package/dist/voice/asr-clients.js.map +1 -0
- package/dist/voice/asr-deepgram.d.ts +38 -0
- package/dist/voice/asr-deepgram.d.ts.map +1 -0
- package/dist/voice/asr-deepgram.js +307 -0
- package/dist/voice/asr-deepgram.js.map +1 -0
- package/dist/voice/asr-webspeech.d.ts +23 -0
- package/dist/voice/asr-webspeech.d.ts.map +1 -0
- package/dist/voice/asr-webspeech.js +93 -0
- package/dist/voice/asr-webspeech.js.map +1 -0
- package/dist/voice/use-dictation.d.ts +6 -0
- package/dist/voice/use-dictation.d.ts.map +1 -0
- package/dist/voice/use-dictation.js +174 -0
- package/dist/voice/use-dictation.js.map +1 -0
- package/dist/voice/voice-store.d.ts +26 -0
- package/dist/voice/voice-store.d.ts.map +1 -0
- package/dist/voice/voice-store.js +27 -0
- package/dist/voice/voice-store.js.map +1 -0
- package/package.json +71 -0
- package/src/components/chat/ask-user-card.tsx +451 -0
- package/src/components/chat/brain-markdown.tsx +547 -0
- package/src/components/chat/chat-page.tsx +820 -0
- package/src/components/chat/command-palette.tsx +68 -0
- package/src/components/chat/markdown-content.tsx +5 -0
- package/src/components/chat/message-bubble.tsx +362 -0
- package/src/components/chat/message-share.ts +78 -0
- package/src/components/chat/renderers/claude-tools.tsx +53 -0
- package/src/components/chat/renderers/generic.tsx +85 -0
- package/src/components/chat/renderers/index.ts +19 -0
- package/src/components/chat/risk-hints.ts +95 -0
- package/src/components/chat/session-drawer.tsx +213 -0
- package/src/components/chat/share-block.tsx +128 -0
- package/src/components/chat/tool-call-timeline.tsx +322 -0
- package/src/components/chat/tool-views.tsx +898 -0
- package/src/components/chat/welcome-state.tsx +105 -0
- package/src/components/connectivity/connection-gate.tsx +137 -0
- package/src/components/connectivity/login-screen.tsx +169 -0
- package/src/components/files/file-panel.tsx +116 -0
- package/src/components/files/file-tree.tsx +143 -0
- package/src/components/files/file-viewer-binary.tsx +63 -0
- package/src/components/files/file-viewer-html.tsx +10 -0
- package/src/components/files/file-viewer-markdown.tsx +14 -0
- package/src/components/files/file-viewer-raw.tsx +44 -0
- package/src/components/files/file-viewer.tsx +290 -0
- package/src/components/files/frontmatter-panel.tsx +78 -0
- package/src/components/layout/app-shell.tsx +25 -0
- package/src/components/layout/mobile-tab-bar.tsx +54 -0
- package/src/components/layout/side-rail.tsx +128 -0
- package/src/components/layout/slide-panel.tsx +64 -0
- package/src/components/quick-actions/streaming-modal.tsx +168 -0
- package/src/components/quick-actions/whatsup-modal.tsx +149 -0
- package/src/components/settings/passkey-panel.tsx +281 -0
- package/src/components/share/share-menu.tsx +121 -0
- package/src/components/voice/dictation-sheet.tsx +217 -0
- package/src/components/voice/mic-button.tsx +31 -0
- package/src/components/voice/review-card.tsx +66 -0
- package/src/config.ts +27 -0
- package/src/hooks/use-vpn-status.ts +75 -0
- package/src/hooks/use-websocket.ts +403 -0
- package/src/index.ts +46 -0
- package/src/lib/api-client.ts +173 -0
- package/src/lib/backend.ts +35 -0
- package/src/lib/frontmatter.ts +99 -0
- package/src/lib/image-attachments.ts +221 -0
- package/src/lib/passkeys.ts +50 -0
- package/src/lib/share.ts +167 -0
- package/src/lib/strip-markdown.ts +38 -0
- package/src/lib/tool-names.ts +27 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/ws-client.ts +109 -0
- package/src/stores/chat-store.ts +619 -0
- package/src/stores/connection-store.ts +23 -0
- package/src/stores/file-store.ts +344 -0
- package/src/stores/provider-store.ts +85 -0
- package/src/stores/ui-store.ts +48 -0
- package/src/styles.css +4 -0
- package/src/theme.css +425 -0
- package/src/voice/asr-clients.ts +45 -0
- package/src/voice/asr-deepgram.ts +325 -0
- package/src/voice/asr-webspeech.ts +135 -0
- package/src/voice/use-dictation.ts +187 -0
- package/src/voice/voice-store.ts +55 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { API_BASE } from "./backend.js";
|
|
2
|
+
import type {
|
|
3
|
+
VoiceKeytermsResponse,
|
|
4
|
+
VoiceTokenResponse,
|
|
5
|
+
VoiceSessionResponse,
|
|
6
|
+
PronunciationOverride,
|
|
7
|
+
ProviderInfo,
|
|
8
|
+
PasskeySummary,
|
|
9
|
+
} from "@schlessera/brain-ui-sdk/protocol";
|
|
10
|
+
import type {
|
|
11
|
+
AuthenticationResponseJSON,
|
|
12
|
+
RegistrationResponseJSON,
|
|
13
|
+
PublicKeyCredentialCreationOptionsJSON,
|
|
14
|
+
PublicKeyCredentialRequestOptionsJSON,
|
|
15
|
+
} from "@simplewebauthn/browser";
|
|
16
|
+
|
|
17
|
+
/** Backend id + capability flags the client renders behavior from. */
|
|
18
|
+
export interface BackendInfo {
|
|
19
|
+
id: string;
|
|
20
|
+
capabilities: {
|
|
21
|
+
concurrentSessions: boolean;
|
|
22
|
+
followUp: boolean;
|
|
23
|
+
[key: string]: boolean;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
|
|
28
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
29
|
+
...init,
|
|
30
|
+
headers: {
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
...init?.headers,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
37
|
+
throw new Error(body.error || `HTTP ${res.status}`);
|
|
38
|
+
}
|
|
39
|
+
return res.json();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const api = {
|
|
43
|
+
health: () =>
|
|
44
|
+
fetchJson<{ status: string; uptime: number; version: string }>("/health"),
|
|
45
|
+
|
|
46
|
+
vpnCheck: () => fetchJson<{ vpn: boolean }>("/vpn-check"),
|
|
47
|
+
|
|
48
|
+
status: () =>
|
|
49
|
+
fetchJson<{
|
|
50
|
+
healthy: boolean;
|
|
51
|
+
uptime: number;
|
|
52
|
+
cronJobs: Array<{
|
|
53
|
+
name: string;
|
|
54
|
+
lastRunAt: number | null;
|
|
55
|
+
lastStatus: string | null;
|
|
56
|
+
}>;
|
|
57
|
+
activeSession: boolean;
|
|
58
|
+
}>("/status"),
|
|
59
|
+
|
|
60
|
+
brainSearch: (q: string, opts?: { type?: string; limit?: number }) =>
|
|
61
|
+
fetchJson<{
|
|
62
|
+
results: Array<{ path: string; title: string; snippet: string; score: number }>;
|
|
63
|
+
warnings: string[];
|
|
64
|
+
}>(
|
|
65
|
+
`/brain/search?q=${encodeURIComponent(q)}${opts?.type ? `&type=${opts.type}` : ""}${opts?.limit ? `&limit=${opts.limit}` : ""}`
|
|
66
|
+
),
|
|
67
|
+
|
|
68
|
+
brainBriefing: () =>
|
|
69
|
+
fetchJson<{ content: string }>("/brain/briefing"),
|
|
70
|
+
|
|
71
|
+
brainStats: () =>
|
|
72
|
+
fetchJson<{
|
|
73
|
+
documents: number;
|
|
74
|
+
byType: Record<string, number>;
|
|
75
|
+
byStatus: Record<string, number>;
|
|
76
|
+
tags: number;
|
|
77
|
+
links: number;
|
|
78
|
+
}>("/brain/stats"),
|
|
79
|
+
|
|
80
|
+
brainSync: () =>
|
|
81
|
+
fetchJson<{ success: boolean; message: string }>("/brain/sync", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
}),
|
|
84
|
+
|
|
85
|
+
brainAdd: (content: string, opts?: { type?: string; title?: string; tags?: string[] }) =>
|
|
86
|
+
fetchJson<{ success: boolean }>("/brain/add", {
|
|
87
|
+
method: "POST",
|
|
88
|
+
body: JSON.stringify({ content, ...opts }),
|
|
89
|
+
}),
|
|
90
|
+
|
|
91
|
+
sessions: () =>
|
|
92
|
+
fetchJson<{
|
|
93
|
+
sessions: Array<{
|
|
94
|
+
id: string;
|
|
95
|
+
title: string | null;
|
|
96
|
+
createdAt: number;
|
|
97
|
+
lastActiveAt: number;
|
|
98
|
+
}>;
|
|
99
|
+
}>("/sessions"),
|
|
100
|
+
|
|
101
|
+
/** Mint a dictation session for the active speech provider. */
|
|
102
|
+
voiceSession: (signal?: AbortSignal) =>
|
|
103
|
+
fetchJson<VoiceSessionResponse>("/voice/session", { method: "POST", signal }),
|
|
104
|
+
|
|
105
|
+
/** @deprecated use voiceSession(); kept during client transition. */
|
|
106
|
+
voiceToken: () =>
|
|
107
|
+
fetchJson<VoiceTokenResponse>("/voice/token", { method: "POST" }),
|
|
108
|
+
|
|
109
|
+
voiceKeyterms: () => fetchJson<VoiceKeytermsResponse>("/voice/keyterms"),
|
|
110
|
+
|
|
111
|
+
voiceOverrides: (signal?: AbortSignal) =>
|
|
112
|
+
fetchJson<{ overrides: PronunciationOverride[] }>("/voice/overrides", {
|
|
113
|
+
signal,
|
|
114
|
+
}),
|
|
115
|
+
|
|
116
|
+
providers: () =>
|
|
117
|
+
fetchJson<{
|
|
118
|
+
providers: ProviderInfo[];
|
|
119
|
+
backends?: Record<string, BackendInfo>;
|
|
120
|
+
}>("/providers"),
|
|
121
|
+
|
|
122
|
+
/** Password-mode login. Resolves on success; throws the server error otherwise. */
|
|
123
|
+
login: (password: string) =>
|
|
124
|
+
fetchJson<{ ok: true }>("/auth/login", {
|
|
125
|
+
method: "POST",
|
|
126
|
+
body: JSON.stringify({ password }),
|
|
127
|
+
}),
|
|
128
|
+
|
|
129
|
+
logout: () =>
|
|
130
|
+
fetchJson<{ ok: true }>("/auth/logout", { method: "POST" }),
|
|
131
|
+
|
|
132
|
+
/** Which login methods the server offers (public; drives the login screen). */
|
|
133
|
+
authMethods: () =>
|
|
134
|
+
fetchJson<{ password: boolean; passkey: boolean }>("/auth/methods"),
|
|
135
|
+
|
|
136
|
+
passkeyLoginOptions: () =>
|
|
137
|
+
fetchJson<PublicKeyCredentialRequestOptionsJSON>("/auth/passkey/login-options", {
|
|
138
|
+
method: "POST",
|
|
139
|
+
body: "{}",
|
|
140
|
+
}),
|
|
141
|
+
|
|
142
|
+
passkeyLoginVerify: (response: AuthenticationResponseJSON) =>
|
|
143
|
+
fetchJson<{ ok: true }>("/auth/passkey/login-verify", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
body: JSON.stringify(response),
|
|
146
|
+
}),
|
|
147
|
+
|
|
148
|
+
passkeyRegisterOptions: () =>
|
|
149
|
+
fetchJson<PublicKeyCredentialCreationOptionsJSON>("/auth/passkey/register-options", {
|
|
150
|
+
method: "POST",
|
|
151
|
+
body: "{}",
|
|
152
|
+
}),
|
|
153
|
+
|
|
154
|
+
passkeyRegisterVerify: (response: RegistrationResponseJSON, label?: string) =>
|
|
155
|
+
fetchJson<{ ok: true; credential: PasskeySummary | null }>(
|
|
156
|
+
"/auth/passkey/register-verify",
|
|
157
|
+
{ method: "POST", body: JSON.stringify({ response, label }) }
|
|
158
|
+
),
|
|
159
|
+
|
|
160
|
+
passkeyList: () =>
|
|
161
|
+
fetchJson<{ credentials: PasskeySummary[] }>("/auth/passkey/list"),
|
|
162
|
+
|
|
163
|
+
passkeyRename: (id: string, label: string) =>
|
|
164
|
+
fetchJson<{ ok: true }>(`/auth/passkey/${encodeURIComponent(id)}`, {
|
|
165
|
+
method: "PUT",
|
|
166
|
+
body: JSON.stringify({ label }),
|
|
167
|
+
}),
|
|
168
|
+
|
|
169
|
+
passkeyDelete: (id: string) =>
|
|
170
|
+
fetchJson<{ ok: true }>(`/auth/passkey/${encodeURIComponent(id)}`, {
|
|
171
|
+
method: "DELETE",
|
|
172
|
+
}),
|
|
173
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend URL configuration.
|
|
3
|
+
*
|
|
4
|
+
* Default topology is SAME-ORIGIN: the server serves the built client and the
|
|
5
|
+
* API/WS from one origin, so API calls go to "/api" and the WebSocket derives
|
|
6
|
+
* its host from window.location. No build-time configuration is needed.
|
|
7
|
+
*
|
|
8
|
+
* VITE_BACKEND_URL is optional advanced config for a SPLIT topology (client and
|
|
9
|
+
* backend on different origins, e.g. a public frontend with the backend reached
|
|
10
|
+
* over a VPN). When set at build time, API and WS calls target that origin.
|
|
11
|
+
* The read is defensive — outside a Vite build (bun test, Node import of the
|
|
12
|
+
* dist) `import.meta.env` does not exist.
|
|
13
|
+
*/
|
|
14
|
+
const viteEnv = (import.meta as { env?: Record<string, string | undefined> }).env;
|
|
15
|
+
const BACKEND_URL = viteEnv?.VITE_BACKEND_URL?.replace(/\/$/, "") ?? "";
|
|
16
|
+
|
|
17
|
+
/** Base URL for API calls. Empty BACKEND_URL = same-origin "/api". */
|
|
18
|
+
export const API_BASE = `${BACKEND_URL}/api`;
|
|
19
|
+
|
|
20
|
+
/** WebSocket URL. Derives wss/ws + host from BACKEND_URL, else same-origin. */
|
|
21
|
+
export function getWsUrl(): string {
|
|
22
|
+
if (BACKEND_URL) {
|
|
23
|
+
const url = new URL("/ws", BACKEND_URL);
|
|
24
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
25
|
+
return url.toString();
|
|
26
|
+
}
|
|
27
|
+
// Same-origin (default).
|
|
28
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
29
|
+
return `${protocol}//${window.location.host}/ws`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Base URL for direct fetch calls (streaming endpoints). */
|
|
33
|
+
export function getBackendUrl(path: string): string {
|
|
34
|
+
return `${BACKEND_URL}${path}`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal YAML-frontmatter splitter for brain repo markdown files.
|
|
3
|
+
*
|
|
4
|
+
* Brain frontmatter is single-line key:value with the occasional inline
|
|
5
|
+
* array (`tags: [a, b, c]`) and quoted string. We don't need a full YAML
|
|
6
|
+
* parser — a small focused split keeps the bundle tiny and the behaviour
|
|
7
|
+
* predictable.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface FrontmatterField {
|
|
11
|
+
key: string;
|
|
12
|
+
/** Raw value as it appears in the file (quotes stripped). */
|
|
13
|
+
value: string;
|
|
14
|
+
/** Parsed array elements when value was `[a, b, c]`-shaped. */
|
|
15
|
+
list?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FrontmatterSplit {
|
|
19
|
+
fields: FrontmatterField[];
|
|
20
|
+
body: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const OPEN = /^---\r?\n/;
|
|
24
|
+
|
|
25
|
+
export function splitFrontmatter(md: string): FrontmatterSplit {
|
|
26
|
+
if (!OPEN.test(md)) return { fields: [], body: md };
|
|
27
|
+
// Find closing `---` on its own line after the opener.
|
|
28
|
+
const afterOpen = md.replace(OPEN, "");
|
|
29
|
+
const closeMatch = afterOpen.match(/\r?\n---\r?\n/);
|
|
30
|
+
if (!closeMatch || closeMatch.index === undefined) {
|
|
31
|
+
return { fields: [], body: md };
|
|
32
|
+
}
|
|
33
|
+
const rawFrontmatter = afterOpen.slice(0, closeMatch.index);
|
|
34
|
+
const body = afterOpen.slice(closeMatch.index + closeMatch[0].length);
|
|
35
|
+
|
|
36
|
+
const fields: FrontmatterField[] = [];
|
|
37
|
+
for (const line of rawFrontmatter.split(/\r?\n/)) {
|
|
38
|
+
if (!line.trim() || line.trimStart().startsWith("#")) continue;
|
|
39
|
+
const sep = line.indexOf(":");
|
|
40
|
+
if (sep < 0) continue;
|
|
41
|
+
const key = line.slice(0, sep).trim();
|
|
42
|
+
if (!key) continue;
|
|
43
|
+
const raw = line.slice(sep + 1).trim();
|
|
44
|
+
fields.push(parseField(key, raw));
|
|
45
|
+
}
|
|
46
|
+
return { fields, body };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseField(key: string, raw: string): FrontmatterField {
|
|
50
|
+
// Inline array: [a, "b", c]
|
|
51
|
+
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
52
|
+
const inner = raw.slice(1, -1);
|
|
53
|
+
const list = splitArray(inner).map(unquote).filter(Boolean);
|
|
54
|
+
return { key, value: raw, list };
|
|
55
|
+
}
|
|
56
|
+
return { key, value: unquote(raw) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function unquote(s: string): string {
|
|
60
|
+
const trimmed = s.trim();
|
|
61
|
+
if (trimmed.length >= 2) {
|
|
62
|
+
const first = trimmed[0];
|
|
63
|
+
const last = trimmed[trimmed.length - 1];
|
|
64
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
65
|
+
return trimmed.slice(1, -1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return trimmed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Split on top-level commas, respecting quoted segments. */
|
|
72
|
+
function splitArray(s: string): string[] {
|
|
73
|
+
const out: string[] = [];
|
|
74
|
+
let buf = "";
|
|
75
|
+
let quote: string | null = null;
|
|
76
|
+
for (let i = 0; i < s.length; i++) {
|
|
77
|
+
const ch = s[i];
|
|
78
|
+
if (quote) {
|
|
79
|
+
buf += ch;
|
|
80
|
+
if (ch === quote && s[i - 1] !== "\\") quote = null;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (ch === '"' || ch === "'") {
|
|
84
|
+
quote = ch;
|
|
85
|
+
buf += ch;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (ch === ",") {
|
|
89
|
+
const piece = buf.trim();
|
|
90
|
+
if (piece) out.push(piece);
|
|
91
|
+
buf = "";
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
buf += ch;
|
|
95
|
+
}
|
|
96
|
+
const tail = buf.trim();
|
|
97
|
+
if (tail) out.push(tail);
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { ChatImageAttachment } from "@schlessera/brain-ui-sdk/protocol";
|
|
2
|
+
import {
|
|
3
|
+
DOWNSCALE_MAX_EDGE,
|
|
4
|
+
MAX_IMAGES_PER_MESSAGE,
|
|
5
|
+
MAX_IMAGE_BYTES,
|
|
6
|
+
MAX_TOTAL_IMAGE_BYTES,
|
|
7
|
+
} from "@schlessera/brain-ui-sdk/protocol";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The vision API silently drops images below a minimum pixel size (a 2x2 PNG
|
|
11
|
+
* is stripped with a misleading "could not be processed" note; 64x64 is fine).
|
|
12
|
+
* Reject anything smaller than this on either edge so the failure is explicit.
|
|
13
|
+
*/
|
|
14
|
+
const MIN_IMAGE_EDGE = 32;
|
|
15
|
+
|
|
16
|
+
/** A processed attachment plus a local object URL for the composer preview. */
|
|
17
|
+
export interface PendingAttachment {
|
|
18
|
+
attachment: ChatImageAttachment;
|
|
19
|
+
previewUrl: string;
|
|
20
|
+
/** Decoded byte length (post-downscale) — used for total-size validation. */
|
|
21
|
+
bytes: number;
|
|
22
|
+
/** Original file name, for error messages. */
|
|
23
|
+
name: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type FileToAttachmentResult =
|
|
27
|
+
| { attachment: ChatImageAttachment; previewUrl: string; bytes: number }
|
|
28
|
+
| { error: string };
|
|
29
|
+
|
|
30
|
+
/** base64 length → decoded byte count (accounts for `=` padding). */
|
|
31
|
+
function decodedBytesFromBase64(b64: string): number {
|
|
32
|
+
const padding = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
|
|
33
|
+
return Math.floor((b64.length * 3) / 4) - padding;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function blobToBase64(blob: Blob): Promise<string> {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const reader = new FileReader();
|
|
39
|
+
reader.onload = () => {
|
|
40
|
+
const result = reader.result as string;
|
|
41
|
+
// Strip the `data:<mime>;base64,` prefix — protocol wants raw base64.
|
|
42
|
+
const comma = result.indexOf(",");
|
|
43
|
+
resolve(comma >= 0 ? result.slice(comma + 1) : result);
|
|
44
|
+
};
|
|
45
|
+
reader.onerror = () => reject(reader.error ?? new Error("read_failed"));
|
|
46
|
+
reader.readAsDataURL(blob);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Decode a file to raw pixels. Prefers `createImageBitmap` (honors EXIF
|
|
52
|
+
* orientation) and falls back to an `<img>` decode when it rejects (e.g.
|
|
53
|
+
* older Safari, some codecs).
|
|
54
|
+
*/
|
|
55
|
+
async function decodeImage(
|
|
56
|
+
file: File
|
|
57
|
+
): Promise<{ width: number; height: number; draw: (ctx: CanvasRenderingContext2D, w: number, h: number) => void } | null> {
|
|
58
|
+
try {
|
|
59
|
+
const bitmap = await createImageBitmap(file, { imageOrientation: "from-image" });
|
|
60
|
+
return {
|
|
61
|
+
width: bitmap.width,
|
|
62
|
+
height: bitmap.height,
|
|
63
|
+
draw: (ctx, w, h) => {
|
|
64
|
+
ctx.drawImage(bitmap, 0, 0, w, h);
|
|
65
|
+
bitmap.close();
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
} catch {
|
|
69
|
+
// Fallback: HTMLImageElement decode via an object URL.
|
|
70
|
+
const url = URL.createObjectURL(file);
|
|
71
|
+
try {
|
|
72
|
+
const img = new Image();
|
|
73
|
+
img.src = url;
|
|
74
|
+
await img.decode();
|
|
75
|
+
const width = img.naturalWidth;
|
|
76
|
+
const height = img.naturalHeight;
|
|
77
|
+
return {
|
|
78
|
+
width,
|
|
79
|
+
height,
|
|
80
|
+
draw: (ctx, w, h) => ctx.drawImage(img, 0, 0, w, h),
|
|
81
|
+
};
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
} finally {
|
|
85
|
+
URL.revokeObjectURL(url);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Turn a picked/pasted image File into a wire-ready attachment plus a preview
|
|
92
|
+
* URL. GIFs pass through un-re-encoded (size-checked only) so animation
|
|
93
|
+
* survives; everything else is downscaled to `DOWNSCALE_MAX_EDGE` and
|
|
94
|
+
* re-encoded to JPEG. Rejects (with a human message) on unsupported types,
|
|
95
|
+
* decode failure, or oversize-after-downscale.
|
|
96
|
+
*/
|
|
97
|
+
export async function fileToAttachment(
|
|
98
|
+
file: File
|
|
99
|
+
): Promise<FileToAttachmentResult> {
|
|
100
|
+
const type = file.type || "";
|
|
101
|
+
|
|
102
|
+
// GIF: keep the raw bytes (canvas would flatten the animation). Measure its
|
|
103
|
+
// dimensions for the min-size guard without re-encoding, then size-check.
|
|
104
|
+
if (type === "image/gif") {
|
|
105
|
+
try {
|
|
106
|
+
const bmp = await createImageBitmap(file);
|
|
107
|
+
const tooSmall = bmp.width < MIN_IMAGE_EDGE || bmp.height < MIN_IMAGE_EDGE;
|
|
108
|
+
bmp.close();
|
|
109
|
+
if (tooSmall) {
|
|
110
|
+
return {
|
|
111
|
+
error: `${file.name}: image too small (min ${MIN_IMAGE_EDGE}px per edge)`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Couldn't measure — fall through and let the size check gate it.
|
|
116
|
+
}
|
|
117
|
+
const data = await blobToBase64(file);
|
|
118
|
+
const bytes = decodedBytesFromBase64(data);
|
|
119
|
+
if (bytes > MAX_IMAGE_BYTES) {
|
|
120
|
+
return { error: `${file.name}: GIF too large (max ${mb(MAX_IMAGE_BYTES)})` };
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
attachment: { data, mediaType: "image/gif" },
|
|
124
|
+
previewUrl: URL.createObjectURL(file),
|
|
125
|
+
bytes,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!type.startsWith("image/")) {
|
|
130
|
+
return { error: `${file.name}: not an image` };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const decoded = await decodeImage(file);
|
|
134
|
+
if (!decoded) {
|
|
135
|
+
return {
|
|
136
|
+
error: `${file.name}: couldn't read this image (unsupported format?)`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { width, height } = decoded;
|
|
141
|
+
if (!width || !height) {
|
|
142
|
+
return { error: `${file.name}: couldn't read this image` };
|
|
143
|
+
}
|
|
144
|
+
if (width < MIN_IMAGE_EDGE || height < MIN_IMAGE_EDGE) {
|
|
145
|
+
return {
|
|
146
|
+
error: `${file.name}: image too small (min ${MIN_IMAGE_EDGE}px per edge)`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Scale so the longest edge is at most DOWNSCALE_MAX_EDGE.
|
|
151
|
+
const scale = Math.min(1, DOWNSCALE_MAX_EDGE / Math.max(width, height));
|
|
152
|
+
const targetW = Math.max(1, Math.round(width * scale));
|
|
153
|
+
const targetH = Math.max(1, Math.round(height * scale));
|
|
154
|
+
|
|
155
|
+
const canvas = document.createElement("canvas");
|
|
156
|
+
canvas.width = targetW;
|
|
157
|
+
canvas.height = targetH;
|
|
158
|
+
const ctx = canvas.getContext("2d");
|
|
159
|
+
if (!ctx) {
|
|
160
|
+
return { error: `${file.name}: canvas unavailable` };
|
|
161
|
+
}
|
|
162
|
+
decoded.draw(ctx, targetW, targetH);
|
|
163
|
+
|
|
164
|
+
const blob = await new Promise<Blob | null>((resolve) =>
|
|
165
|
+
canvas.toBlob(resolve, "image/jpeg", 0.85)
|
|
166
|
+
);
|
|
167
|
+
if (!blob) {
|
|
168
|
+
return { error: `${file.name}: failed to encode` };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const data = await blobToBase64(blob);
|
|
172
|
+
const bytes = decodedBytesFromBase64(data);
|
|
173
|
+
if (bytes > MAX_IMAGE_BYTES) {
|
|
174
|
+
return {
|
|
175
|
+
error: `${file.name}: still ${mb(bytes)} after downscale (max ${mb(MAX_IMAGE_BYTES)})`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
attachment: { data, mediaType: "image/jpeg" },
|
|
181
|
+
previewUrl: URL.createObjectURL(blob),
|
|
182
|
+
bytes,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Enforce message-level caps against a proposed set of attachments (existing
|
|
188
|
+
* pending + newly added). Returns the accepted subset and per-file rejection
|
|
189
|
+
* reasons; never mutates the input.
|
|
190
|
+
*/
|
|
191
|
+
export function validateAttachments(pending: PendingAttachment[]): {
|
|
192
|
+
accepted: PendingAttachment[];
|
|
193
|
+
errors: string[];
|
|
194
|
+
} {
|
|
195
|
+
const accepted: PendingAttachment[] = [];
|
|
196
|
+
const errors: string[] = [];
|
|
197
|
+
let total = 0;
|
|
198
|
+
|
|
199
|
+
for (const item of pending) {
|
|
200
|
+
if (accepted.length >= MAX_IMAGES_PER_MESSAGE) {
|
|
201
|
+
errors.push(
|
|
202
|
+
`${item.name}: over the ${MAX_IMAGES_PER_MESSAGE}-image limit`
|
|
203
|
+
);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (total + item.bytes > MAX_TOTAL_IMAGE_BYTES) {
|
|
207
|
+
errors.push(
|
|
208
|
+
`${item.name}: over the ${mb(MAX_TOTAL_IMAGE_BYTES)} total-size limit`
|
|
209
|
+
);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
total += item.bytes;
|
|
213
|
+
accepted.push(item);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { accepted, errors };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function mb(bytes: number): string {
|
|
220
|
+
return `${(bytes / 1_000_000).toFixed(1)} MB`;
|
|
221
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
startAuthentication,
|
|
3
|
+
startRegistration,
|
|
4
|
+
browserSupportsWebAuthn,
|
|
5
|
+
browserSupportsWebAuthnAutofill,
|
|
6
|
+
} from "@simplewebauthn/browser";
|
|
7
|
+
import { api } from "./api-client.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Thin ceremony wrappers around @simplewebauthn/browser. All server calls go
|
|
11
|
+
* through api-client; success means the server set the session cookie.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export function supportsPasskeys(): boolean {
|
|
15
|
+
return browserSupportsWebAuthn();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function supportsAutofill(): Promise<boolean> {
|
|
19
|
+
return browserSupportsWebAuthnAutofill();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** True for the user dismissing/cancelling the OS passkey prompt. */
|
|
23
|
+
export function isUserCancel(err: unknown): boolean {
|
|
24
|
+
return (
|
|
25
|
+
err instanceof Error &&
|
|
26
|
+
(err.name === "NotAllowedError" || err.name === "AbortError")
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Full login ceremony. With `useBrowserAutofill`, resolves only if the user
|
|
32
|
+
* picks a passkey from the input's autofill suggestions (conditional UI).
|
|
33
|
+
*/
|
|
34
|
+
export async function loginWithPasskey(opts?: {
|
|
35
|
+
useBrowserAutofill?: boolean;
|
|
36
|
+
}): Promise<void> {
|
|
37
|
+
const optionsJSON = await api.passkeyLoginOptions();
|
|
38
|
+
const response = await startAuthentication({
|
|
39
|
+
optionsJSON,
|
|
40
|
+
useBrowserAutofill: opts?.useBrowserAutofill ?? false,
|
|
41
|
+
});
|
|
42
|
+
await api.passkeyLoginVerify(response);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Full registration ceremony (requires an authenticated session). */
|
|
46
|
+
export async function registerPasskey(label?: string): Promise<void> {
|
|
47
|
+
const optionsJSON = await api.passkeyRegisterOptions();
|
|
48
|
+
const response = await startRegistration({ optionsJSON });
|
|
49
|
+
await api.passkeyRegisterVerify(response, label);
|
|
50
|
+
}
|