@flamingo-stack/openframe-frontend-core 0.0.568 → 0.0.569
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/dist/{chunk-P7QGTNEW.cjs → chunk-2DSDGWWU.cjs} +101 -101
- package/dist/{chunk-P7QGTNEW.cjs.map → chunk-2DSDGWWU.cjs.map} +1 -1
- package/dist/{chunk-ZARYIFCI.js → chunk-3UPQAOES.js} +5 -3
- package/dist/{chunk-ZARYIFCI.js.map → chunk-3UPQAOES.js.map} +1 -1
- package/dist/{chunk-KL6ZDLDF.js → chunk-4NN7V443.js} +2 -2
- package/dist/{chunk-G2PAUMV6.js → chunk-6STF67EF.js} +4 -4
- package/dist/{chunk-6RSKK4E7.cjs → chunk-74DB5H5T.cjs} +1258 -1162
- package/dist/chunk-74DB5H5T.cjs.map +1 -0
- package/dist/{chunk-6TDWCBQM.js → chunk-A2T266F3.js} +7 -7
- package/dist/{chunk-HKG7BZON.cjs → chunk-AAROYQ4O.cjs} +14 -14
- package/dist/{chunk-HKG7BZON.cjs.map → chunk-AAROYQ4O.cjs.map} +1 -1
- package/dist/{chunk-CEAAOVCV.cjs → chunk-AJPGQCZ2.cjs} +1 -1
- package/dist/chunk-AJPGQCZ2.cjs.map +1 -0
- package/dist/{chunk-ZXASQL3E.js → chunk-C5GKBUT7.js} +4 -4
- package/dist/{chunk-RBYMJF2V.cjs → chunk-D6SBYELJ.cjs} +35 -35
- package/dist/{chunk-RBYMJF2V.cjs.map → chunk-D6SBYELJ.cjs.map} +1 -1
- package/dist/{chunk-LNUXUOPN.cjs → chunk-EBE22KOQ.cjs} +4 -4
- package/dist/{chunk-LNUXUOPN.cjs.map → chunk-EBE22KOQ.cjs.map} +1 -1
- package/dist/{chunk-EYXZF46V.js → chunk-FE5QR7PT.js} +995 -899
- package/dist/chunk-FE5QR7PT.js.map +1 -0
- package/dist/{chunk-6PWUUWCO.cjs → chunk-FMWIA5IH.cjs} +42 -42
- package/dist/{chunk-6PWUUWCO.cjs.map → chunk-FMWIA5IH.cjs.map} +1 -1
- package/dist/{chunk-DHTT77AG.cjs → chunk-GY22CIEF.cjs} +78 -78
- package/dist/{chunk-DHTT77AG.cjs.map → chunk-GY22CIEF.cjs.map} +1 -1
- package/dist/{chunk-O3JCKEVG.js → chunk-HOMYI5RR.js} +2 -2
- package/dist/{chunk-BZ7B255K.js → chunk-KCAGMTJ6.js} +9 -9
- package/dist/{chunk-DZ2Z7HRF.js → chunk-KO333Q2B.js} +3 -3
- package/dist/{chunk-MQQM4NCN.js → chunk-LJBNHO6B.js} +3 -3
- package/dist/{chunk-KZS2DFC2.cjs → chunk-ME346OJF.cjs} +42 -42
- package/dist/{chunk-KZS2DFC2.cjs.map → chunk-ME346OJF.cjs.map} +1 -1
- package/dist/{chunk-UWY722RH.js → chunk-MR6RRXSD.js} +1 -1
- package/dist/chunk-MR6RRXSD.js.map +1 -0
- package/dist/{chunk-ZIZJDXUI.js → chunk-MZG3QQUR.js} +2 -2
- package/dist/{chunk-HB3IG2MC.js → chunk-OS5RFMW5.js} +3 -3
- package/dist/{chunk-VEXTUGUA.js → chunk-OWCN3BM6.js} +7 -7
- package/dist/{chunk-QVQLNKFS.js → chunk-P7ZENKIA.js} +2 -2
- package/dist/{chunk-T7AE7CKV.cjs → chunk-PCL6WYYT.cjs} +7 -7
- package/dist/{chunk-T7AE7CKV.cjs.map → chunk-PCL6WYYT.cjs.map} +1 -1
- package/dist/{chunk-2LKVYI7H.js → chunk-PLJAOZYZ.js} +6 -6
- package/dist/{chunk-L22CSZZU.cjs → chunk-QY3VTOB6.cjs} +18 -18
- package/dist/{chunk-L22CSZZU.cjs.map → chunk-QY3VTOB6.cjs.map} +1 -1
- package/dist/{chunk-Y2BYHTEN.cjs → chunk-SMW5Z4NW.cjs} +23 -23
- package/dist/{chunk-Y2BYHTEN.cjs.map → chunk-SMW5Z4NW.cjs.map} +1 -1
- package/dist/{chunk-LXWQLUBR.cjs → chunk-SW3K3JSP.cjs} +9 -7
- package/dist/chunk-SW3K3JSP.cjs.map +1 -0
- package/dist/{chunk-NKEOD2AD.cjs → chunk-TDYTAED2.cjs} +3 -3
- package/dist/{chunk-NKEOD2AD.cjs.map → chunk-TDYTAED2.cjs.map} +1 -1
- package/dist/{chunk-ZNEVGXMU.js → chunk-TXEI4PES.js} +112 -4
- package/dist/chunk-TXEI4PES.js.map +1 -0
- package/dist/{chunk-KZ3PIUCH.cjs → chunk-VNE2EHCD.cjs} +14 -14
- package/dist/{chunk-KZ3PIUCH.cjs.map → chunk-VNE2EHCD.cjs.map} +1 -1
- package/dist/{chunk-6I76UK2Z.cjs → chunk-XMECOGLY.cjs} +3 -3
- package/dist/{chunk-6I76UK2Z.cjs.map → chunk-XMECOGLY.cjs.map} +1 -1
- package/dist/{chunk-NK3M6F5A.cjs → chunk-Z3SRMLMK.cjs} +121 -13
- package/dist/chunk-Z3SRMLMK.cjs.map +1 -0
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/index.cjs +4 -17
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +3 -16
- package/dist/components/chat/utils/index.d.ts +0 -2
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/claude-embed.d.ts +15 -8
- package/dist/components/embeds/claude-embed.d.ts.map +1 -1
- package/dist/components/embeds/embed-viewer-frame.d.ts +8 -1
- package/dist/components/embeds/embed-viewer-frame.d.ts.map +1 -1
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +25 -25
- package/dist/components/help-center-pages/index.js +16 -16
- package/dist/components/index.cjs +142 -154
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +14 -26
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +36 -36
- package/dist/components/meeting-scheduler/index.js +5 -5
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +8 -8
- package/dist/components/tickets/index.js +7 -7
- package/dist/components/ui/index.cjs +4 -16
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +3 -15
- package/dist/components/ui/markdown/sanitize.d.ts.map +1 -1
- package/dist/components/ui/table/table.d.ts +1 -1
- package/dist/components/ui/table/table.d.ts.map +1 -1
- package/dist/components/ui/table/types.d.ts +9 -0
- package/dist/components/ui/table/types.d.ts.map +1 -1
- package/dist/contexts/endpoints-runtime-context.d.ts +6 -0
- package/dist/contexts/endpoints-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +2 -2
- package/dist/contexts/index.js +1 -1
- package/dist/hooks/index.cjs +5 -3
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +4 -2
- package/dist/hooks/use-claude-mirror-src.d.ts +17 -0
- package/dist/hooks/use-claude-mirror-src.d.ts.map +1 -0
- package/dist/index.cjs +10 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +15 -13
- package/dist/utils/embed-proxy-auth-storage.d.ts.map +1 -1
- package/dist/utils/embed-url-converters.d.ts +20 -0
- package/dist/utils/embed-url-converters.d.ts.map +1 -1
- package/dist/utils/fullscreen-switch.d.ts +85 -0
- package/dist/utils/fullscreen-switch.d.ts.map +1 -0
- package/dist/utils/index.cjs +114 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +113 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/hooks/use-chat-attachments.ts +3 -3
- package/src/components/chat/hooks/use-chat-history-hydration.ts +2 -2
- package/src/components/chat/hooks/use-chat-identity.ts +5 -5
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +3 -3
- package/src/components/chat/utils/.index.md +1 -2
- package/src/components/chat/utils/chat-conversation-storage.ts +2 -2
- package/src/components/chat/utils/index.ts +3 -10
- package/src/components/embeds/claude-embed.tsx +41 -14
- package/src/components/embeds/embed-viewer-frame.tsx +77 -4
- package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +30 -0
- package/src/components/ui/markdown/rich/__tests__/shortcodes.test.ts +8 -0
- package/src/components/ui/markdown/sanitize.ts +4 -0
- package/src/components/ui/table/table.tsx +7 -2
- package/src/components/ui/table/types.ts +9 -0
- package/src/contexts/endpoints-runtime-context.tsx +6 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/use-claude-mirror-src.ts +43 -0
- package/src/utils/.embed-proxy-auth-storage.md +2 -2
- package/src/utils/embed-proxy-auth-storage.ts +19 -14
- package/src/utils/embed-url-converters.ts +45 -0
- package/src/utils/fullscreen-switch.ts +180 -0
- package/src/utils/index.ts +8 -6
- package/dist/chunk-6RSKK4E7.cjs.map +0 -1
- package/dist/chunk-CEAAOVCV.cjs.map +0 -1
- package/dist/chunk-EYXZF46V.js.map +0 -1
- package/dist/chunk-LXWQLUBR.cjs.map +0 -1
- package/dist/chunk-NK3M6F5A.cjs.map +0 -1
- package/dist/chunk-UWY722RH.js.map +0 -1
- package/dist/chunk-ZNEVGXMU.js.map +0 -1
- package/dist/components/chat/utils/chat-authed-fetch.d.ts +0 -15
- package/dist/components/chat/utils/chat-authed-fetch.d.ts.map +0 -1
- package/dist/components/chat/utils/chat-proxy-auth-storage.d.ts +0 -16
- package/dist/components/chat/utils/chat-proxy-auth-storage.d.ts.map +0 -1
- package/src/components/chat/utils/.chat-authed-fetch.md +0 -25
- package/src/components/chat/utils/.chat-proxy-auth-storage.md +0 -38
- package/src/components/chat/utils/chat-authed-fetch.ts +0 -17
- package/src/components/chat/utils/chat-proxy-auth-storage.ts +0 -25
- /package/dist/{chunk-KL6ZDLDF.js.map → chunk-4NN7V443.js.map} +0 -0
- /package/dist/{chunk-G2PAUMV6.js.map → chunk-6STF67EF.js.map} +0 -0
- /package/dist/{chunk-6TDWCBQM.js.map → chunk-A2T266F3.js.map} +0 -0
- /package/dist/{chunk-ZXASQL3E.js.map → chunk-C5GKBUT7.js.map} +0 -0
- /package/dist/{chunk-O3JCKEVG.js.map → chunk-HOMYI5RR.js.map} +0 -0
- /package/dist/{chunk-BZ7B255K.js.map → chunk-KCAGMTJ6.js.map} +0 -0
- /package/dist/{chunk-DZ2Z7HRF.js.map → chunk-KO333Q2B.js.map} +0 -0
- /package/dist/{chunk-MQQM4NCN.js.map → chunk-LJBNHO6B.js.map} +0 -0
- /package/dist/{chunk-ZIZJDXUI.js.map → chunk-MZG3QQUR.js.map} +0 -0
- /package/dist/{chunk-HB3IG2MC.js.map → chunk-OS5RFMW5.js.map} +0 -0
- /package/dist/{chunk-VEXTUGUA.js.map → chunk-OWCN3BM6.js.map} +0 -0
- /package/dist/{chunk-QVQLNKFS.js.map → chunk-P7ZENKIA.js.map} +0 -0
- /package/dist/{chunk-2LKVYI7H.js.map → chunk-PLJAOZYZ.js.map} +0 -0
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `AbortController` for clean remove-mid-upload.
|
|
10
10
|
*
|
|
11
11
|
* Auth propagation:
|
|
12
|
-
* - `
|
|
12
|
+
* - `embedAuthedFetch` carries bearer-act-as headers + Supabase
|
|
13
13
|
* session cookies — used both for minting the upload URL and as
|
|
14
14
|
* the auth path before falling through to the signed PUT.
|
|
15
15
|
* - The byte-PUT to the signed URL uses an inline `uploadWithProgress`
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
27
27
|
import { fileTypeFromBlob } from 'file-type'
|
|
28
28
|
import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'
|
|
29
|
-
import {
|
|
29
|
+
import { embedAuthedFetch } from '../../../utils/embed-authed-fetch'
|
|
30
30
|
import {
|
|
31
31
|
CHAT_ATTACHMENT_MIME_TYPES,
|
|
32
32
|
CHAT_ATTACHMENT_CONCURRENT_UPLOADS_PER_USER,
|
|
@@ -243,7 +243,7 @@ export function useChatAttachments(): UseChatAttachmentsApi {
|
|
|
243
243
|
// bearer-act-as headers (if present).
|
|
244
244
|
updateOne(att.id, { status: 'uploading', progress: 5 })
|
|
245
245
|
|
|
246
|
-
const urlResp = await
|
|
246
|
+
const urlResp = await embedAuthedFetch(uploadUrlEndpoint, {
|
|
247
247
|
method: 'POST',
|
|
248
248
|
headers: { 'Content-Type': 'application/json' },
|
|
249
249
|
signal: ctrl?.signal,
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { useEffect, useRef, useState, type MutableRefObject } from 'react'
|
|
21
21
|
import type { Message } from './use-chat'
|
|
22
|
-
import {
|
|
22
|
+
import { embedAuthedFetch } from '../../../utils/embed-authed-fetch'
|
|
23
23
|
import { AUTO_CONTINUATION_DIRECTIVE_PREFIX } from '../utils/auto-continuation-directive'
|
|
24
24
|
|
|
25
25
|
export interface UseChatHistoryHydrationArgs {
|
|
@@ -79,7 +79,7 @@ export function useChatHistoryHydration({
|
|
|
79
79
|
setIsHydratingHistory(true)
|
|
80
80
|
;(async () => {
|
|
81
81
|
try {
|
|
82
|
-
const res = await
|
|
82
|
+
const res = await embedAuthedFetch(
|
|
83
83
|
`${historyUrl}?conversationId=${encodeURIComponent(conversationId)}`,
|
|
84
84
|
{ method: 'GET' },
|
|
85
85
|
)
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
|
|
44
44
|
import { createContext, createElement, useContext, useEffect, useState, type ReactNode } from 'react'
|
|
45
45
|
import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
46
|
+
import { embedAuthedFetch } from '../../../utils/embed-authed-fetch'
|
|
47
|
+
import { getEmbedProxyAuth } from '../../../utils/embed-proxy-auth-storage'
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Wire-shape for the identity route response. Mirrors the hub's
|
|
@@ -109,10 +109,10 @@ const ANON_DEFAULTS: ChatIdentityResponse = {
|
|
|
109
109
|
function useResolveChatIdentity(enabled: boolean): ChatIdentitySurface {
|
|
110
110
|
const runtime = useRequiredChatRuntime()
|
|
111
111
|
const url = runtime.endpoints.identityUrl
|
|
112
|
-
// `
|
|
112
|
+
// `getEmbedProxyAuth()` reads localStorage every render. If the user
|
|
113
113
|
// pastes bearer creds mid-session (via the `/debug` creds bar),
|
|
114
114
|
// their email arrives here and the effect's dep changes → refetch.
|
|
115
|
-
const proxyEmail =
|
|
115
|
+
const proxyEmail = getEmbedProxyAuth()?.email ?? null
|
|
116
116
|
|
|
117
117
|
const [data, setData] = useState<ChatIdentityResponse>(ANON_DEFAULTS)
|
|
118
118
|
// Only "loading" if THIS instance will actually fetch. A disabled
|
|
@@ -126,7 +126,7 @@ function useResolveChatIdentity(enabled: boolean): ChatIdentitySurface {
|
|
|
126
126
|
const ctrl = new AbortController()
|
|
127
127
|
|
|
128
128
|
setIsLoading(true)
|
|
129
|
-
|
|
129
|
+
embedAuthedFetch(url, { signal: ctrl.signal })
|
|
130
130
|
.then(async (resp) => {
|
|
131
131
|
if (!resp.ok) return ANON_DEFAULTS
|
|
132
132
|
return (await resp.json()) as ChatIdentityResponse
|
|
@@ -57,7 +57,7 @@ import type { ChatRef } from '../chat-ref.types'
|
|
|
57
57
|
import type { Message } from '../types/message.types'
|
|
58
58
|
import type { MessageSegment } from '../types/message.types'
|
|
59
59
|
import { useSlashCommandRegistry, type SlashCommandSummary } from './use-slash-commands'
|
|
60
|
-
import {
|
|
60
|
+
import { embedAuthedFetch } from '../../../utils/embed-authed-fetch'
|
|
61
61
|
import type { ScrollAnchor } from '../utils/scroll-anchor'
|
|
62
62
|
import { buildConfirmToolBody, readServerErrorMessage } from '../../../chat-protocol/confirm-tool'
|
|
63
63
|
import { createSseFrameDecoder } from '../../../chat-protocol/decode'
|
|
@@ -458,10 +458,10 @@ export function useSseChatAdapter(
|
|
|
458
458
|
abortControllerRef.current = ctrl
|
|
459
459
|
|
|
460
460
|
try {
|
|
461
|
-
// `
|
|
461
|
+
// `embedAuthedFetch` carries the bearer-act-as headers (+ Supabase
|
|
462
462
|
// session cookies) — same wrapper `use-chat-attachments` and
|
|
463
463
|
// `use-chat-identity` use.
|
|
464
|
-
const response = await
|
|
464
|
+
const response = await embedAuthedFetch(targetPath, {
|
|
465
465
|
method: 'POST',
|
|
466
466
|
body: JSON.stringify(requestBody),
|
|
467
467
|
signal: ctrl.signal,
|
|
@@ -8,7 +8,7 @@ Barrel export module that consolidates and re-exports all message processing, ch
|
|
|
8
8
|
| **Message Accumulation** | `MessageSegmentAccumulator`, `createMessageSegmentAccumulator`, `AccumulatorCallbacks` |
|
|
9
9
|
| **Historical Messages** | `processHistoricalMessages`, `extractErrorMessages`, `processHistoricalMessagesWithErrors` |
|
|
10
10
|
| **Chat Navigation** | `handleChatNavClick`, `resolveExternalNavigation`, `decideNewTab`, `computeIsNewTab`, `buildAnchorProps` |
|
|
11
|
-
| **Proxy Auth** | `
|
|
11
|
+
| **Proxy Auth** | moved to `utils/` — `getEmbedProxyAuth`, `setEmbedProxyAuth`, `clearEmbedProxyAuth`, `applyProxyAuth`, `embedAuthedFetch` |
|
|
12
12
|
| **Attachments** | `formatChatAttachmentMarkdownForBubble`, `stripChatAttachmentMarkdown`, `buildChatAttachmentViewUrl` |
|
|
13
13
|
| **UI / Styling** | `chatChipClass`, compact card class constants (`COMPACT_CARD_*`), `getStatusColorScheme` |
|
|
14
14
|
| **Icon Registry** | `ICON_REGISTRY`, `getIconComponent`, `normalizeIconKey`, `getDynamicIcon` |
|
|
@@ -21,7 +21,6 @@ Barrel export module that consolidates and re-exports all message processing, ch
|
|
|
21
21
|
import {
|
|
22
22
|
createMessageSegmentAccumulator,
|
|
23
23
|
handleChatNavClick,
|
|
24
|
-
getChatProxyAuth,
|
|
25
24
|
getIconComponent,
|
|
26
25
|
formatChatAttachmentMarkdownForBubble,
|
|
27
26
|
decideNewTab,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
createLocalStorageAdapter,
|
|
20
20
|
type LocalStorageAdapter,
|
|
21
21
|
} from '../../../utils/local-storage-adapter'
|
|
22
|
-
import {
|
|
22
|
+
import { getEmbedProxyAuth } from '../../../utils/embed-proxy-auth-storage'
|
|
23
23
|
|
|
24
24
|
export interface PersistedChatConversation {
|
|
25
25
|
conversationId: string
|
|
@@ -29,7 +29,7 @@ const STORAGE_KEY = 'conversation'
|
|
|
29
29
|
|
|
30
30
|
const namespaceFor = (source: string): string => {
|
|
31
31
|
const base = `mingo-chat-${source}`
|
|
32
|
-
const auth =
|
|
32
|
+
const auth = getEmbedProxyAuth()
|
|
33
33
|
return auth?.email ? `${base}-u-${encodeURIComponent(auth.email.toLowerCase())}` : base
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -89,16 +89,9 @@ export {
|
|
|
89
89
|
type RuntimeMode,
|
|
90
90
|
} from './decide-new-tab'
|
|
91
91
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
getPersistedProxyEmail,
|
|
96
|
-
setChatProxyAuth,
|
|
97
|
-
clearChatProxyAuth,
|
|
98
|
-
applyProxyAuth,
|
|
99
|
-
type ChatProxyAuth,
|
|
100
|
-
} from './chat-proxy-auth-storage'
|
|
101
|
-
export { chatAuthedFetch } from './chat-authed-fetch'
|
|
92
|
+
// Embed-surface auth moved to `utils/` (embed-proxy-auth-storage /
|
|
93
|
+
// embed-authed-fetch) — import the Embed* names from
|
|
94
|
+
// `@flamingo-stack/openframe-frontend-core/utils`.
|
|
102
95
|
|
|
103
96
|
// Pure content helpers
|
|
104
97
|
export { flattenAssistantContent } from './flatten-assistant-content'
|
|
@@ -4,6 +4,7 @@ import { ExternalLink } from 'lucide-react'
|
|
|
4
4
|
import { Button } from '../ui/button/button'
|
|
5
5
|
import { ClaudeIcon } from '../icons/claude-icon'
|
|
6
6
|
import { EmbedViewerFrame } from './embed-viewer-frame'
|
|
7
|
+
import { useClaudeMirrorSrc } from '../../hooks/use-claude-mirror-src'
|
|
7
8
|
import { toClaudeEmbedUrl } from '../../utils/embed-url-converters'
|
|
8
9
|
|
|
9
10
|
export type ClaudeEmbedKind = 'artifact' | 'design'
|
|
@@ -33,18 +34,29 @@ const KIND_HEADING: Record<ClaudeEmbedKind, string> = {
|
|
|
33
34
|
* `{{claude-artifact:URL}}` / `{{claude-design:URL}}` shortcodes, so it looks
|
|
34
35
|
* identical wherever markdown renders — a spec body, a comment, a links rail.
|
|
35
36
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* the
|
|
37
|
+
* FRAME SOURCE — fully under the hood, in priority order:
|
|
38
|
+
* 1. The host's SELF-HOSTED MIRROR: the url's artifact id derives the
|
|
39
|
+
* storage-view proxy path (`toClaudeMirrorPath`), probed with a
|
|
40
|
+
* 1-byte ranged fetch. When the host has ingested a copy, the frame
|
|
41
|
+
* shows it — claude.ai frame-locks CODE artifacts entirely, so this
|
|
42
|
+
* is the only way those ever render inline.
|
|
43
|
+
* 2. claude.ai's own `/embed` route for a PUBLISHED artifact
|
|
44
|
+
* (`toClaudeEmbedUrl`).
|
|
45
|
+
* 3. Neither → the shared frame's empty state.
|
|
46
|
+
* The consumer passes only the claude URL; "Open in Claude" always
|
|
47
|
+
* targets it, so commenting stays on claude.ai.
|
|
40
48
|
*
|
|
41
|
-
* What defeats the
|
|
42
|
-
* and its `/embed` path answers `frame-ancestors 'self'`),
|
|
43
|
-
* (no embed route), an artifact whose author has not
|
|
44
|
-
* or an http host (mixed content
|
|
49
|
+
* What defeats the claude.ai leg: a Claude CODE url (no Embed settings
|
|
50
|
+
* exist for it, and its `/embed` path answers `frame-ancestors 'self'`),
|
|
51
|
+
* a Claude Design url (no embed route), an artifact whose author has not
|
|
52
|
+
* allow-listed this host, or an http host (mixed content).
|
|
45
53
|
*/
|
|
46
54
|
export function ClaudeEmbed({ url, kind = 'artifact', title, height, loading = 'lazy' }: ClaudeEmbedProps) {
|
|
47
|
-
|
|
55
|
+
// Mirror detection lives in `useClaudeMirrorSrc` (derive from
|
|
56
|
+
// EndpointsRuntime + 1-byte ranged probe) — this component stays
|
|
57
|
+
// presentational: mirror when the hook found one, claude.ai otherwise.
|
|
58
|
+
const mirrorSrc = useClaudeMirrorSrc(url)
|
|
59
|
+
const embedUrl = mirrorSrc ?? toClaudeEmbedUrl(url)
|
|
48
60
|
return (
|
|
49
61
|
<EmbedViewerFrame
|
|
50
62
|
className="my-6 space-y-3"
|
|
@@ -65,6 +77,10 @@ export function ClaudeEmbed({ url, kind = 'artifact', title, height, loading = '
|
|
|
65
77
|
</Button>
|
|
66
78
|
}
|
|
67
79
|
src={embedUrl}
|
|
80
|
+
// The framed artifact has no internal fullscreen button of its own
|
|
81
|
+
// (unlike Figma's player) — the shell provides the toggle for both
|
|
82
|
+
// the claude.ai `/embed` route and a mirror.
|
|
83
|
+
fullscreenControl
|
|
68
84
|
loading={loading}
|
|
69
85
|
height={height}
|
|
70
86
|
// Same frame contract Figma gets — an artifact is a real app: it copies
|
|
@@ -72,11 +88,22 @@ export function ClaudeEmbed({ url, kind = 'artifact', title, height, loading = '
|
|
|
72
88
|
allow="clipboard-write; clipboard-read; fullscreen"
|
|
73
89
|
// …but UNLIKE Figma, an artifact is USER-AUTHORED HTML and JS, so it is
|
|
74
90
|
// sandboxed. Omitting `allow-top-navigation` is the point: an artifact
|
|
75
|
-
// cannot navigate the page it is embedded in. `allow-same-origin`
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
91
|
+
// cannot navigate the page it is embedded in. The `allow-same-origin`
|
|
92
|
+
// token is CONDITIONAL on where the frame points:
|
|
93
|
+
// - claude.ai `/embed` (no mirror): Anthropic's own embed snippet
|
|
94
|
+
// uses it, and on a CROSS-ORIGIN frame it grants the frame
|
|
95
|
+
// claude.ai's origin — never ours. The artifact runtime needs
|
|
96
|
+
// it (storage, postMessage handshake).
|
|
97
|
+
// - the derived MIRROR is served from the HOST's OWN origin —
|
|
98
|
+
// `allow-same-origin` + `allow-scripts` there would be a no-op
|
|
99
|
+
// sandbox handing the artifact first-party cookies and
|
|
100
|
+
// `window.parent`. A static mirror renders fine from an opaque
|
|
101
|
+
// origin, so the token is dropped.
|
|
102
|
+
sandbox={
|
|
103
|
+
mirrorSrc
|
|
104
|
+
? 'allow-scripts allow-popups allow-forms'
|
|
105
|
+
: 'allow-scripts allow-same-origin allow-popups allow-forms'
|
|
106
|
+
}
|
|
80
107
|
allowFullScreen
|
|
81
108
|
emptyIcon={<ClaudeIcon className="w-16 h-16 mb-4" />}
|
|
82
109
|
emptyMessage="Open this one in Claude · it has no embeddable view"
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import React from 'react'
|
|
3
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { Maximize2, Minimize2 } from 'lucide-react'
|
|
4
5
|
import { cn } from '../../utils/cn'
|
|
6
|
+
import { FullscreenSwitchController } from '../../utils/fullscreen-switch'
|
|
7
|
+
import { Button } from '../ui/button/button'
|
|
5
8
|
import { EmbedIframe } from './embed-iframe'
|
|
6
9
|
|
|
7
10
|
/**
|
|
@@ -47,6 +50,13 @@ export interface EmbedViewerFrameProps {
|
|
|
47
50
|
sandbox?: string
|
|
48
51
|
/** Wrapper classes; defaults to the sheets/pdf `space-y-4`. */
|
|
49
52
|
className?: string
|
|
53
|
+
/** Opt-in fullscreen toggle in the header row (rendered after `actions`,
|
|
54
|
+
* only when there is a `src`). Fullscreens the WHOLE frame — header
|
|
55
|
+
* included — so the exit control stays reachable (Esc works too).
|
|
56
|
+
* Exists because some embedded contents ship their own fullscreen
|
|
57
|
+
* button (the Figma player) and some cannot (a self-hosted Claude
|
|
58
|
+
* artifact mirror) — the shell provides the affordance uniformly. */
|
|
59
|
+
fullscreenControl?: boolean
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
export function EmbedViewerFrame({
|
|
@@ -63,9 +73,45 @@ export function EmbedViewerFrame({
|
|
|
63
73
|
loading,
|
|
64
74
|
sandbox,
|
|
65
75
|
className,
|
|
76
|
+
fullscreenControl,
|
|
66
77
|
}: EmbedViewerFrameProps) {
|
|
78
|
+
const frameRef = useRef<HTMLDivElement>(null)
|
|
79
|
+
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
80
|
+
|
|
81
|
+
// ONE fullscreen implementation across the library and its hosts:
|
|
82
|
+
// `FullscreenSwitchController` (shared with the hub's deck) owns the
|
|
83
|
+
// Fullscreen API calls, webkit fallbacks, rejection safety, and the
|
|
84
|
+
// change-event-driven state — Esc, F11, buttons and browser chrome all
|
|
85
|
+
// converge on its listener, so the icon can never desync. Here it runs
|
|
86
|
+
// element-level (`target` = this frame; no mask classes — the frame
|
|
87
|
+
// styles off its own state).
|
|
88
|
+
const controllerRef = useRef<FullscreenSwitchController | null>(null)
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!fullscreenControl) return
|
|
91
|
+
const controller = new FullscreenSwitchController({
|
|
92
|
+
target: () => frameRef.current,
|
|
93
|
+
onFullscreenChange: setIsFullscreen,
|
|
94
|
+
})
|
|
95
|
+
controllerRef.current = controller
|
|
96
|
+
controller.attach()
|
|
97
|
+
return () => {
|
|
98
|
+
controllerRef.current = null
|
|
99
|
+
controller.detach()
|
|
100
|
+
}
|
|
101
|
+
}, [fullscreenControl])
|
|
102
|
+
const toggleFullscreen = () => controllerRef.current?.toggle()
|
|
103
|
+
|
|
67
104
|
return (
|
|
68
|
-
<div
|
|
105
|
+
<div
|
|
106
|
+
ref={frameRef}
|
|
107
|
+
className={cn(
|
|
108
|
+
'space-y-[var(--spacing-system-mf)]',
|
|
109
|
+
className,
|
|
110
|
+
// Fullscreen paints its own ground (the fullscreened element
|
|
111
|
+
// otherwise sits on the UA's black backdrop) and scrolls itself.
|
|
112
|
+
isFullscreen && 'overflow-auto bg-ods-bg p-[var(--spacing-system-mf)]',
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
69
115
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
70
116
|
<div className="flex items-center gap-2 min-w-0">
|
|
71
117
|
{icon}
|
|
@@ -75,13 +121,40 @@ export function EmbedViewerFrame({
|
|
|
75
121
|
<span className="text-h6 font-semibold text-ods-text-primary truncate">{title}</span>
|
|
76
122
|
)}
|
|
77
123
|
</div>
|
|
78
|
-
{
|
|
124
|
+
{/* Same responsive action-row idiom as the PDF viewer's button pair
|
|
125
|
+
and every viewer's own action: full-width stacked on mobile,
|
|
126
|
+
inline row from `sm:` up, SAME Button idiom (outline /
|
|
127
|
+
small-legacy / w-4 icon / w-full sm:w-auto). Without the
|
|
128
|
+
fullscreen toggle, `actions` renders EXACTLY as before — the
|
|
129
|
+
wrapper exists only when the toggle joins it, so pre-existing
|
|
130
|
+
viewers (figma/pdf/sheets) are byte-identical. */}
|
|
131
|
+
{fullscreenControl && src ? (
|
|
132
|
+
<div className="flex flex-col gap-[var(--spacing-system-xsf)] sm:flex-row sm:items-center">
|
|
133
|
+
{actions}
|
|
134
|
+
<Button
|
|
135
|
+
variant="outline"
|
|
136
|
+
size="small-legacy"
|
|
137
|
+
onClick={toggleFullscreen}
|
|
138
|
+
aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
139
|
+
leftIcon={isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
|
140
|
+
className="w-full sm:w-auto"
|
|
141
|
+
>
|
|
142
|
+
{isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
|
143
|
+
</Button>
|
|
144
|
+
</div>
|
|
145
|
+
) : (
|
|
146
|
+
actions
|
|
147
|
+
)}
|
|
79
148
|
</div>
|
|
80
149
|
{src ? (
|
|
81
150
|
<EmbedIframe
|
|
82
151
|
src={src}
|
|
83
152
|
title={title}
|
|
84
|
-
|
|
153
|
+
// 96px = the fullscreen wrapper's own chrome above the iframe:
|
|
154
|
+
// header row (~40px incl. its gap) + the wrapper's mf padding
|
|
155
|
+
// (16px × 2) + the mf space-y gap (16px), rounded up so the
|
|
156
|
+
// frame never forces a scrollbar. Recheck if that chrome changes.
|
|
157
|
+
height={isFullscreen ? 'calc(100vh - 96px)' : height}
|
|
85
158
|
allow={allow}
|
|
86
159
|
allowFullScreen={allowFullScreen}
|
|
87
160
|
loading={loading}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
buildEffectiveTagSet,
|
|
17
17
|
buildSanitizeSchema,
|
|
18
18
|
escapeUnknownHtmlTags,
|
|
19
|
+
rehypeStripUnsafe,
|
|
19
20
|
SAFE_HTML_TAGS,
|
|
20
21
|
SVG_TAGS,
|
|
21
22
|
} from '../sanitize'
|
|
@@ -352,3 +353,32 @@ describe('link-definition shelters blank in every shape (round 21)', () => {
|
|
|
352
353
|
expect(masked).toContain('## after heading')
|
|
353
354
|
})
|
|
354
355
|
})
|
|
356
|
+
|
|
357
|
+
describe('URL_ATTRS covers hast PROPERTY keys, not attribute names', () => {
|
|
358
|
+
it('strips a javascript: data-url via its camelized hast key', () => {
|
|
359
|
+
// property-information camelizes `data-url` to `dataUrl` BEFORE the
|
|
360
|
+
// strip pass runs - the URL_ATTRS entry must match the lowercased
|
|
361
|
+
// PROPERTY key or the scheme check silently never fires.
|
|
362
|
+
const tree = {
|
|
363
|
+
type: 'root',
|
|
364
|
+
children: [
|
|
365
|
+
{
|
|
366
|
+
type: 'element',
|
|
367
|
+
tagName: 'div',
|
|
368
|
+
properties: {
|
|
369
|
+
className: ['claude-embed'],
|
|
370
|
+
dataUrl: 'javascript:alert(1)',
|
|
371
|
+
dataKind: 'artifact',
|
|
372
|
+
},
|
|
373
|
+
children: [],
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
}
|
|
377
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
378
|
+
;(rehypeStripUnsafe() as (t: unknown) => void)(tree as any)
|
|
379
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
380
|
+
const props = (tree.children[0] as any).properties
|
|
381
|
+
expect(props.dataUrl).toBeUndefined()
|
|
382
|
+
expect(props.dataKind).toBe('artifact')
|
|
383
|
+
})
|
|
384
|
+
})
|
|
@@ -91,6 +91,14 @@ describe('processShortcodes design-doc embeds', () => {
|
|
|
91
91
|
expect(design).toContain('data-kind="design"');
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
it('threads the optional pipe title into data-title', () => {
|
|
95
|
+
const titled = processShortcodes('{{claude-artifact:https://claude.ai/a|Brief doc}}');
|
|
96
|
+
expect(titled).toContain('data-title="Brief doc"');
|
|
97
|
+
|
|
98
|
+
const untitled = processShortcodes('{{claude-artifact:https://claude.ai/a}}');
|
|
99
|
+
expect(untitled).not.toContain('data-title');
|
|
100
|
+
});
|
|
101
|
+
|
|
94
102
|
it('escapes the url attribute so a crafted link cannot break out of the block', () => {
|
|
95
103
|
const output = processShortcodes('{{claude-artifact:https://claude.ai/a"><script>alert(1)</script>}}');
|
|
96
104
|
expect(output).not.toContain('<script>');
|
|
@@ -395,6 +395,10 @@ const URL_ATTRS = new Set([
|
|
|
395
395
|
'data',
|
|
396
396
|
'action',
|
|
397
397
|
'background',
|
|
398
|
+
// The embed shortcodes' primary url (`data-url` → `dataUrl`) — it
|
|
399
|
+
// becomes an anchor href ("Open in Claude") and a link-preview fetch
|
|
400
|
+
// target, so a `javascript:`/`data:` scheme there is a live sink too.
|
|
401
|
+
'dataurl',
|
|
398
402
|
])
|
|
399
403
|
|
|
400
404
|
/**
|
|
@@ -21,6 +21,7 @@ function injectSyntheticColumns<T>(
|
|
|
21
21
|
rowActions?: RowAction<T>[],
|
|
22
22
|
renderRowActions?: (item: T) => ReactNode,
|
|
23
23
|
rowHref?: (item: T) => string | null | undefined,
|
|
24
|
+
actionsColumnWidth?: string,
|
|
24
25
|
): TableColumn<T>[] {
|
|
25
26
|
const hasActions = Boolean(rowActions?.length) || Boolean(renderRowActions)
|
|
26
27
|
const result = [...columns]
|
|
@@ -29,7 +30,10 @@ function injectSyntheticColumns<T>(
|
|
|
29
30
|
const actionsColumn: TableColumn<T> = {
|
|
30
31
|
key: '__actions__',
|
|
31
32
|
label: '',
|
|
32
|
-
|
|
33
|
+
// A caller-fixed width keeps the header and every row on ONE grid
|
|
34
|
+
// (see `actionsColumnWidth` in types.ts); the default stays
|
|
35
|
+
// content-sized for back-compat.
|
|
36
|
+
width: actionsColumnWidth ?? 'min-w-[100px] w-auto shrink-0 flex-none',
|
|
33
37
|
align: 'right',
|
|
34
38
|
renderCell: (item: T) => (
|
|
35
39
|
<div className="flex gap-2 items-center justify-end pointer-events-auto" data-no-row-click>
|
|
@@ -120,11 +124,12 @@ export function Table<T = any>({
|
|
|
120
124
|
stickyHeader,
|
|
121
125
|
stickyHeaderOffset,
|
|
122
126
|
animateRowReorder,
|
|
127
|
+
actionsColumnWidth,
|
|
123
128
|
}: TableProps<T>) {
|
|
124
129
|
// Opt-in row-reorder animation: framer-motion is loaded lazily (its own chunk)
|
|
125
130
|
// ONLY when `animateRowReorder` is set, so the default table stays motion-free.
|
|
126
131
|
const tableMotion = useTableMotion(Boolean(animateRowReorder))
|
|
127
|
-
const columnsWithActions = injectSyntheticColumns(columns, rowActions, renderRowActions, rowHref)
|
|
132
|
+
const columnsWithActions = injectSyntheticColumns(columns, rowActions, renderRowActions, rowHref, actionsColumnWidth)
|
|
128
133
|
const getRowHref = (item: T): string | undefined => {
|
|
129
134
|
if (onRowClick || !rowHref) return undefined
|
|
130
135
|
return rowHref(item) ?? undefined
|
|
@@ -110,6 +110,15 @@ export interface TableProps<T = any> {
|
|
|
110
110
|
rowActions?: RowAction<T>[]
|
|
111
111
|
// Custom renderer for the actions area (e.g., kebab/dots menu)
|
|
112
112
|
renderRowActions?: (item: T) => ReactNode
|
|
113
|
+
/**
|
|
114
|
+
* Width class(es) for the synthetic actions column. The default is
|
|
115
|
+
* content-sized (`w-auto`), which mis-aligns every OTHER column between
|
|
116
|
+
* the header and the rows whenever the row actions are wider than the
|
|
117
|
+
* header's "Showing N results" counter (each side sizes to its own
|
|
118
|
+
* content). Pass a FIXED width (e.g. `w-60`) when the actions are wide
|
|
119
|
+
* text buttons so the header and rows share one grid.
|
|
120
|
+
*/
|
|
121
|
+
actionsColumnWidth?: string
|
|
113
122
|
|
|
114
123
|
/**
|
|
115
124
|
* When provided, renders a right-pointing chevron link at the end of every row
|
|
@@ -34,6 +34,12 @@ export interface EndpointsRuntime {
|
|
|
34
34
|
}
|
|
35
35
|
/** POST contact-form submission. */
|
|
36
36
|
contactUrl: string
|
|
37
|
+
/** GET base of the host's private-storage view proxy
|
|
38
|
+
* (`<base>/<bucket>/<object>`). `ClaudeEmbed` derives artifact mirror
|
|
39
|
+
* urls under it (`<base>/design-briefs/<uuid>.html`). OPTIONAL: a host
|
|
40
|
+
* without the proxy simply omits it and claude embeds fall back to
|
|
41
|
+
* claude.ai — no probing happens. */
|
|
42
|
+
storageViewBaseUrl?: string
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
export const EndpointsRuntimeContext = createContext<EndpointsRuntime | null>(null)
|
package/src/hooks/index.ts
CHANGED
|
@@ -29,6 +29,10 @@ export * from './use-access-code-integration'
|
|
|
29
29
|
// THE og-placeholder URL hook (endpoints-driven; no injected builder)
|
|
30
30
|
export * from './use-og-placeholder-url'
|
|
31
31
|
|
|
32
|
+
// Claude artifact mirror detection (endpoints-driven; ClaudeEmbed's
|
|
33
|
+
// transparent mirror leg)
|
|
34
|
+
export * from './use-claude-mirror-src'
|
|
35
|
+
|
|
32
36
|
// Deep-link "scroll to URL hash" after data loads. Pairs with URL
|
|
33
37
|
// composers that emit `?<filter>=<id>#<prefix>-<id>` — the filter
|
|
34
38
|
// narrows the list, the hash scrolls the matching DOM id.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useEndpointsRuntime } from '../contexts/endpoints-runtime-context'
|
|
5
|
+
import { toClaudeMirrorPath } from '../utils/embed-url-converters'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The host's SELF-HOSTED MIRROR src for a claude artifact url, or `null`
|
|
9
|
+
* while none is known — THE hook behind `ClaudeEmbed`'s transparent
|
|
10
|
+
* mirror leg, extracted so the component stays presentational.
|
|
11
|
+
*
|
|
12
|
+
* Fully under the hood: derives the candidate mirror path from the url's
|
|
13
|
+
* artifact id under the host's configured storage-view proxy base
|
|
14
|
+
* (`EndpointsRuntime.storageViewBaseUrl` — the SAME proxy-path mechanism
|
|
15
|
+
* as every other lib→API call) and probes it with a 1-byte ranged GET
|
|
16
|
+
* (the proxy forwards Range, so the probe costs nothing). Exists → the
|
|
17
|
+
* mirror path; missing / no proxy / network blip → `null`, and the
|
|
18
|
+
* caller's claude.ai fallback is exactly the pre-mirror behavior. A host
|
|
19
|
+
* that mounts no EndpointsRuntime provider (or omits the entry) skips
|
|
20
|
+
* the mirror leg entirely.
|
|
21
|
+
*/
|
|
22
|
+
export function useClaudeMirrorSrc(url: string): string | null {
|
|
23
|
+
const endpoints = useEndpointsRuntime()
|
|
24
|
+
const mirrorPath = toClaudeMirrorPath(url, endpoints?.storageViewBaseUrl)
|
|
25
|
+
const [mirrorSrc, setMirrorSrc] = useState<string | null>(null)
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
setMirrorSrc(null)
|
|
28
|
+
if (!mirrorPath) return
|
|
29
|
+
let cancelled = false
|
|
30
|
+
fetch(mirrorPath, { headers: { Range: 'bytes=0-0' } })
|
|
31
|
+
.then(res => {
|
|
32
|
+
if (!cancelled && (res.ok || res.status === 206)) setMirrorSrc(mirrorPath)
|
|
33
|
+
})
|
|
34
|
+
.catch(() => {
|
|
35
|
+
// No proxy on this host / network blip — the caller's claude.ai
|
|
36
|
+
// fallback stands, same as before mirrors existed.
|
|
37
|
+
})
|
|
38
|
+
return () => {
|
|
39
|
+
cancelled = true
|
|
40
|
+
}
|
|
41
|
+
}, [mirrorPath])
|
|
42
|
+
return mirrorSrc
|
|
43
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- source-hash: 4f6996d4ad0ba284b64e13807891eb62 -->
|
|
2
|
-
Manages client-side persistence of embed-surface proxy credentials (
|
|
2
|
+
Manages client-side persistence of embed-surface proxy credentials (a platform API key + impersonation email) in `localStorage`, enabling embedded surfaces (chat widget, ticket center, etc.) to authenticate as a proxied customer across sessions.
|
|
3
3
|
|
|
4
4
|
## Key Components
|
|
5
5
|
|
|
@@ -35,4 +35,4 @@ const response = await fetch(url, { method: 'POST', headers })
|
|
|
35
35
|
clearEmbedProxyAuth()
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
> **Security note:** Credentials persist in `localStorage` (survives tab close and browser restart). This is intentional — re-pasting on every tab was rejected as a dev-experience tradeoff. The `/debug` entry point is admin-gated, and all headers are server-validated against `
|
|
38
|
+
> **Security note:** Credentials persist in `localStorage` (survives tab close and browser restart). This is intentional — re-pasting on every tab was rejected as a dev-experience tradeoff. The `/debug` entry point is admin-gated, and all headers are server-validated against the hash-at-rest platform API key store (revocable in `/admin/api-keys`).
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Client-side persistence for embed-surface proxy credentials
|
|
5
|
-
*
|
|
4
|
+
* Client-side persistence for embed-surface proxy credentials (a
|
|
5
|
+
* PLATFORM API KEY + impersonation email). Used by every embedded
|
|
6
6
|
* surface — the chat widget AND the ticket center AND any future
|
|
7
7
|
* embedded React component that needs to identify itself as the
|
|
8
8
|
* impersonated customer.
|
|
9
9
|
*
|
|
10
10
|
* When set, the surface attaches the creds as
|
|
11
|
-
* `Authorization: Bearer <
|
|
11
|
+
* `Authorization: Bearer <platform API key>` + `X-Chat-Act-As: <email>`
|
|
12
12
|
* on every call to `/api/docs/chat`, `/api/chat/*`, and any other route
|
|
13
13
|
* gated by `requireChatAuth` — proving to the server that this session
|
|
14
|
-
* is acting on behalf of <email>.
|
|
14
|
+
* is acting on behalf of <email>. The key is minted in the hub's
|
|
15
|
+
* `/admin/api-keys` and is platform-bound (`fpk_<platform>_…`): a
|
|
16
|
+
* SERVICE key may act as any email; a PERSONAL key only as its owner.
|
|
15
17
|
*
|
|
16
|
-
* **Naming history:** the
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* env
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* **Naming history:** the stored field is still named `secret` and the
|
|
19
|
+
* wire-side header names are `X-Chat-*`. Those are client/server
|
|
20
|
+
* contracts predating the per-platform key migration (the old shared
|
|
21
|
+
* `CHAT_PROXY_SECRET` env is removed hub-side); renaming them would
|
|
22
|
+
* break persisted state + the wire shape for zero behavior change. The
|
|
23
|
+
* CLIENT-side helpers were renamed `Embed*` so non-chat surfaces (e.g.
|
|
24
|
+
* ticket center) don't have to import a chat-prefixed symbol just to
|
|
25
|
+
* send the same headers.
|
|
22
26
|
*
|
|
23
27
|
* Persists to **`localStorage`** so the bearer token + act-as identity
|
|
24
28
|
* survive tab close, new-tab opens, and browser restarts — the
|
|
@@ -26,10 +30,11 @@
|
|
|
26
30
|
* cycle was rejected as a dev-experience tradeoff that wasn't worth
|
|
27
31
|
* the security gain. An XSS sink on this origin can read the value
|
|
28
32
|
* indefinitely (vs only-this-tab with sessionStorage), but `/debug`
|
|
29
|
-
* is admin-gated behind the platform's `askAI.enabled` flag
|
|
30
|
-
*
|
|
31
|
-
* `
|
|
32
|
-
*
|
|
33
|
+
* is admin-gated behind the platform's `askAI.enabled` flag, the key
|
|
34
|
+
* is server-verified (hash-at-rest, platform-bound, revocable in
|
|
35
|
+
* `/admin/api-keys`), and the act-as reach is bounded by the key's
|
|
36
|
+
* tier. Explicit "Clear" button on the creds bar is the supported
|
|
37
|
+
* logout path; closing the tab is no longer.
|
|
33
38
|
*
|
|
34
39
|
* Namespaced under `<platform>.chat.proxy-auth.v1` (the storage key is
|
|
35
40
|
* unchanged from the old chat-prefixed helper — that's a storage
|