@flamingo-stack/openframe-frontend-core 0.0.557 → 0.0.558-1830.4885.1
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-A6TAVIBX.cjs → chunk-3TKG7SW6.cjs} +42 -42
- package/dist/{chunk-A6TAVIBX.cjs.map → chunk-3TKG7SW6.cjs.map} +1 -1
- package/dist/{chunk-SBKX2VSG.js → chunk-4VLGJEKQ.js} +2 -2
- package/dist/{chunk-V5EYGEX7.cjs → chunk-53GNFRVQ.cjs} +7 -7
- package/dist/{chunk-V5EYGEX7.cjs.map → chunk-53GNFRVQ.cjs.map} +1 -1
- package/dist/{chunk-FXVMRZIQ.js → chunk-5JHJX7OX.js} +5 -5
- package/dist/{chunk-6YXEZCVG.cjs → chunk-5WQPOH27.cjs} +5 -5
- package/dist/{chunk-6YXEZCVG.cjs.map → chunk-5WQPOH27.cjs.map} +1 -1
- package/dist/{chunk-RPCY2FAC.js → chunk-C5HQCF4R.js} +6 -6
- package/dist/{chunk-4WREPWRL.cjs → chunk-CWQSUYVV.cjs} +3 -3
- package/dist/{chunk-4WREPWRL.cjs.map → chunk-CWQSUYVV.cjs.map} +1 -1
- package/dist/{chunk-33FOBAWS.cjs → chunk-DHSIAIFG.cjs} +18 -18
- package/dist/{chunk-33FOBAWS.cjs.map → chunk-DHSIAIFG.cjs.map} +1 -1
- package/dist/{chunk-VJL46NHW.js → chunk-DNPQ2U7P.js} +4 -4
- package/dist/{chunk-G7ROZAY5.js → chunk-DV4CSNZD.js} +3 -3
- package/dist/{chunk-25YLMIWU.js → chunk-E26KVZJM.js} +3 -3
- package/dist/{chunk-YHRE4JBF.cjs → chunk-FCCSHC3H.cjs} +32 -32
- package/dist/{chunk-YHRE4JBF.cjs.map → chunk-FCCSHC3H.cjs.map} +1 -1
- package/dist/{chunk-DRNSIK3V.js → chunk-FVHFBEXH.js} +2 -2
- package/dist/{chunk-6ZOVSD4M.cjs → chunk-GYQHUJWR.cjs} +14 -14
- package/dist/{chunk-6ZOVSD4M.cjs.map → chunk-GYQHUJWR.cjs.map} +1 -1
- package/dist/{chunk-G3NBTL4I.js → chunk-IHXUZBBV.js} +6 -4
- package/dist/{chunk-G3NBTL4I.js.map → chunk-IHXUZBBV.js.map} +1 -1
- package/dist/{chunk-WBQ3NOH7.js → chunk-KL6ZDLDF.js} +2 -2
- package/dist/{chunk-Y6TXTBIQ.js → chunk-LM6NDYGD.js} +2 -2
- package/dist/{chunk-5HNZTPAB.cjs → chunk-N3OGB543.cjs} +12 -12
- package/dist/{chunk-5HNZTPAB.cjs.map → chunk-N3OGB543.cjs.map} +1 -1
- package/dist/{chunk-VJTD2DGF.cjs → chunk-NK3M6F5A.cjs} +39 -30
- package/dist/chunk-NK3M6F5A.cjs.map +1 -0
- package/dist/{chunk-DFJ7TWH6.cjs → chunk-NKEOD2AD.cjs} +3 -3
- package/dist/{chunk-DFJ7TWH6.cjs.map → chunk-NKEOD2AD.cjs.map} +1 -1
- package/dist/{chunk-5JF6H7OJ.cjs → chunk-R3OCP4A7.cjs} +76 -76
- package/dist/{chunk-5JF6H7OJ.cjs.map → chunk-R3OCP4A7.cjs.map} +1 -1
- package/dist/{chunk-IPO24G7U.cjs → chunk-R3TXAMMB.cjs} +143 -141
- package/dist/{chunk-IPO24G7U.cjs.map → chunk-R3TXAMMB.cjs.map} +1 -1
- package/dist/{chunk-LJWTXODF.js → chunk-S3NZZKNU.js} +3 -3
- package/dist/{chunk-BE6TRUKY.cjs → chunk-SI64DC72.cjs} +40 -40
- package/dist/{chunk-BE6TRUKY.cjs.map → chunk-SI64DC72.cjs.map} +1 -1
- package/dist/{chunk-NNGQ6ONX.js → chunk-UVGFICZT.js} +8 -8
- package/dist/{chunk-4EGNYOZA.cjs → chunk-VNWGXQJD.cjs} +23 -23
- package/dist/{chunk-4EGNYOZA.cjs.map → chunk-VNWGXQJD.cjs.map} +1 -1
- package/dist/{chunk-QCBBX6DO.cjs → chunk-XDMRN3SG.cjs} +97 -97
- package/dist/{chunk-QCBBX6DO.cjs.map → chunk-XDMRN3SG.cjs.map} +1 -1
- package/dist/{chunk-64HHDGRN.cjs → chunk-XL2G2HHJ.cjs} +4 -4
- package/dist/{chunk-64HHDGRN.cjs.map → chunk-XL2G2HHJ.cjs.map} +1 -1
- package/dist/{chunk-UTEQHK3H.js → chunk-YUVCH2LX.js} +6 -6
- package/dist/{chunk-26HAJBSQ.js → chunk-Z5FN5VTC.js} +2 -2
- package/dist/{chunk-VRX6Y6A7.js → chunk-ZNEVGXMU.js} +23 -14
- package/dist/chunk-ZNEVGXMU.js.map +1 -0
- package/dist/{chunk-RRWWAH37.js → chunk-ZSZ24UY6.js} +2 -2
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/features/video.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +24 -24
- package/dist/components/help-center-pages/index.js +15 -15
- package/dist/components/index.cjs +137 -137
- package/dist/components/index.js +13 -13
- package/dist/components/meeting-scheduler/index.cjs +34 -34
- package/dist/components/meeting-scheduler/index.js +4 -4
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +7 -7
- package/dist/components/onboarding-guides/index.js +6 -6
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/related-content/index.js +4 -4
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/hooks/index.cjs +6 -2
- 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 +5 -1
- package/dist/hooks/use-authed-asset-src.d.ts +16 -0
- package/dist/hooks/use-authed-asset-src.d.ts.map +1 -0
- package/dist/hooks/use-authed-image-src.d.ts +15 -6
- package/dist/hooks/use-authed-image-src.d.ts.map +1 -1
- package/dist/index.cjs +7 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6 -2
- package/dist/utils/embed-authed-fetch.d.ts +24 -10
- package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/features/video.tsx +13 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-authed-asset-src.ts +98 -0
- package/src/hooks/use-authed-image-src.ts +15 -84
- package/src/utils/__tests__/embed-authed-fetch.test.ts +50 -1
- package/src/utils/embed-authed-fetch.ts +31 -14
- package/dist/chunk-VJTD2DGF.cjs.map +0 -1
- package/dist/chunk-VRX6Y6A7.js.map +0 -1
- /package/dist/{chunk-SBKX2VSG.js.map → chunk-4VLGJEKQ.js.map} +0 -0
- /package/dist/{chunk-FXVMRZIQ.js.map → chunk-5JHJX7OX.js.map} +0 -0
- /package/dist/{chunk-RPCY2FAC.js.map → chunk-C5HQCF4R.js.map} +0 -0
- /package/dist/{chunk-VJL46NHW.js.map → chunk-DNPQ2U7P.js.map} +0 -0
- /package/dist/{chunk-G7ROZAY5.js.map → chunk-DV4CSNZD.js.map} +0 -0
- /package/dist/{chunk-25YLMIWU.js.map → chunk-E26KVZJM.js.map} +0 -0
- /package/dist/{chunk-DRNSIK3V.js.map → chunk-FVHFBEXH.js.map} +0 -0
- /package/dist/{chunk-WBQ3NOH7.js.map → chunk-KL6ZDLDF.js.map} +0 -0
- /package/dist/{chunk-Y6TXTBIQ.js.map → chunk-LM6NDYGD.js.map} +0 -0
- /package/dist/{chunk-LJWTXODF.js.map → chunk-S3NZZKNU.js.map} +0 -0
- /package/dist/{chunk-NNGQ6ONX.js.map → chunk-UVGFICZT.js.map} +0 -0
- /package/dist/{chunk-UTEQHK3H.js.map → chunk-YUVCH2LX.js.map} +0 -0
- /package/dist/{chunk-26HAJBSQ.js.map → chunk-Z5FN5VTC.js.map} +0 -0
- /package/dist/{chunk-RRWWAH37.js.map → chunk-ZSZ24UY6.js.map} +0 -0
package/package.json
CHANGED
|
@@ -36,6 +36,7 @@ import { VideoPlayBadge, VideoUnmuteGlyph } from './video-center-badge';
|
|
|
36
36
|
import { fetchPriorityProp } from '../../utils/fetch-priority';
|
|
37
37
|
import { useIosNativeVideoFullscreen } from './use-ios-native-video-fullscreen';
|
|
38
38
|
import { saveDataEnabled } from './use-video-warmup';
|
|
39
|
+
import { useAuthedAssetSrc } from '../../hooks/use-authed-asset-src';
|
|
39
40
|
|
|
40
41
|
// =============================================================================
|
|
41
42
|
// Dev-only hover→playing latency instrumentation gate. Always on in dev
|
|
@@ -647,6 +648,16 @@ function FilePlayer({
|
|
|
647
648
|
// an early manifest fetch is at most one bounded manifest+segment — the
|
|
648
649
|
// Save-Data verification gate covers it (see plan A2.4).
|
|
649
650
|
const effectivePreload = preload ?? (saveDataEnabled() ? 'none' : 'metadata');
|
|
651
|
+
// THE captions seam. `<track src>` is a browser subresource: it carries no
|
|
652
|
+
// custom headers, so it authenticates by cookie only. Fine on the cookie-auth
|
|
653
|
+
// web, a guaranteed 401 on every HEADER-auth host — native shells and
|
|
654
|
+
// dev-ticket web — whose caption route sits behind the same proxied
|
|
655
|
+
// `/content/api/captions` path as every other embedded endpoint. Resolving it
|
|
656
|
+
// through `useAuthedAssetSrc` puts the track on the SAME single auth knob as
|
|
657
|
+
// every embedded `fetch` (adapter bearer + 401-refresh-retry), so a host
|
|
658
|
+
// configures `endpoints.captionsUrlPrefix` and nothing else — no per-surface,
|
|
659
|
+
// per-host caption plumbing. Cookie-auth hosts get the URL back untouched.
|
|
660
|
+
const resolvedCaptionsUrl = useAuthedAssetSrc(captionsUrl, 'text/vtt, */*');
|
|
650
661
|
// True while hover playback is running MUTED because the browser's autoplay
|
|
651
662
|
// policy blocked sound (no user activation yet). Drives the center unmute
|
|
652
663
|
// control — the industry pattern (muted autoplay + explicit unmute button)
|
|
@@ -1030,10 +1041,10 @@ function FilePlayer({
|
|
|
1030
1041
|
...(fit === 'cover' ? ({ '--media-object-fit': 'cover' } as React.CSSProperties) : {}),
|
|
1031
1042
|
}}
|
|
1032
1043
|
>
|
|
1033
|
-
{
|
|
1044
|
+
{resolvedCaptionsUrl ? (
|
|
1034
1045
|
<track
|
|
1035
1046
|
kind="captions"
|
|
1036
|
-
src={
|
|
1047
|
+
src={resolvedCaptionsUrl}
|
|
1037
1048
|
srcLang="en"
|
|
1038
1049
|
label="English"
|
|
1039
1050
|
default
|
package/src/hooks/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './use-copy-to-clipboard'
|
|
|
8
8
|
// Image authentication hooks
|
|
9
9
|
export * from './use-batch-images'
|
|
10
10
|
export * from './use-authenticated-image'
|
|
11
|
+
export * from './use-authed-asset-src'
|
|
11
12
|
export * from './use-authed-image-src'
|
|
12
13
|
|
|
13
14
|
// URL State Management hooks
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useReducer } from 'react'
|
|
4
|
+
import { embedAuthedFetch, needsBearerAssetFetch } from '../utils/embed-authed-fetch'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve ANY asset URL the browser would load natively — `<img src>`,
|
|
8
|
+
* `<track src>`, a font, a poster — for hosts whose auth rides in request
|
|
9
|
+
* HEADERS instead of cookies: native shells (`capacitor://`, `tauri://`) and
|
|
10
|
+
* dev-ticket web, both in bearer mode. A native subresource load can't carry
|
|
11
|
+
* an `Authorization` header, so a gateway URL that works on the cookie-auth
|
|
12
|
+
* web 401s there.
|
|
13
|
+
*
|
|
14
|
+
* When `needsBearerAssetFetch` says the URL is bearer-authed, this hook
|
|
15
|
+
* fetches it through `embedAuthedFetch` (bearer + deduped 401-refresh-retry +
|
|
16
|
+
* the adapter's cross-origin guard — the SAME single auth knob every embedded
|
|
17
|
+
* `fetch` in the lib rides) and returns a blob object-URL. Every other URL is
|
|
18
|
+
* returned unchanged, so on the cookie-auth web the hook is a pass-through.
|
|
19
|
+
*
|
|
20
|
+
* Returns `undefined` while the fetch is in flight OR after it fails, so
|
|
21
|
+
* callers show their existing placeholder / "no captions" branch instead of a
|
|
22
|
+
* broken asset; a failed fetch retries on the next mount.
|
|
23
|
+
*
|
|
24
|
+
* Cache: module-level, session-lifetime, keyed by full URL — so the two
|
|
25
|
+
* `<track>` URLs of one player, or the same avatar in twenty rows, cost one
|
|
26
|
+
* request. Gateway URLs carry a `?v=<content-hash>` cache-buster, so changed
|
|
27
|
+
* content is a new key; entries are never revoked (a VTT / avatar is a few KB
|
|
28
|
+
* — refcount churn isn't worth it) and are dropped wholesale at session end
|
|
29
|
+
* via `clearAuthedAssetCache`.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const resolvedCache = new Map<string, string>()
|
|
33
|
+
const inFlight = new Map<string, Promise<void>>()
|
|
34
|
+
let cacheGeneration = 0
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Drop every cached blob and revoke its object URL. Hosts call this at
|
|
38
|
+
* session end (logout / forced re-login) so a follow-on login as a
|
|
39
|
+
* different identity cannot be served blobs fetched under the previous
|
|
40
|
+
* one's bearer. Fetches still in flight when the clear happens are fenced
|
|
41
|
+
* by a generation counter — their results are revoked instead of cached.
|
|
42
|
+
*/
|
|
43
|
+
export function clearAuthedAssetCache(): void {
|
|
44
|
+
cacheGeneration++
|
|
45
|
+
for (const url of resolvedCache.values()) URL.revokeObjectURL(url)
|
|
46
|
+
resolvedCache.clear()
|
|
47
|
+
inFlight.clear()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fetchAsBlobUrl(src: string, accept: string): Promise<void> {
|
|
51
|
+
let pending = inFlight.get(src)
|
|
52
|
+
if (!pending) {
|
|
53
|
+
const startedGeneration = cacheGeneration
|
|
54
|
+
pending = embedAuthedFetch(src, { headers: { Accept: accept } })
|
|
55
|
+
.then(async response => {
|
|
56
|
+
if (!response.ok) throw new Error(`asset fetch failed: ${response.status}`)
|
|
57
|
+
const blobUrl = URL.createObjectURL(await response.blob())
|
|
58
|
+
if (startedGeneration === cacheGeneration) {
|
|
59
|
+
resolvedCache.set(src, blobUrl)
|
|
60
|
+
} else {
|
|
61
|
+
URL.revokeObjectURL(blobUrl)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
.finally(() => {
|
|
65
|
+
if (inFlight.get(src) === pending) inFlight.delete(src)
|
|
66
|
+
})
|
|
67
|
+
inFlight.set(src, pending)
|
|
68
|
+
}
|
|
69
|
+
return pending
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param src the asset URL (relative or absolute), or null/undefined
|
|
74
|
+
* @param accept `Accept` header for the authed fetch — narrow it to what the
|
|
75
|
+
* consumer renders (`image/*`, `text/vtt`); it also replaces
|
|
76
|
+
* `embedAuthedFetch`'s default JSON `Content-Type` on the GET.
|
|
77
|
+
*/
|
|
78
|
+
export function useAuthedAssetSrc(src?: string | null, accept = '*/*'): string | undefined {
|
|
79
|
+
const bearerSrc = src && needsBearerAssetFetch(src) ? src : null
|
|
80
|
+
const [, rerender] = useReducer((c: number) => c + 1, 0)
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!bearerSrc || resolvedCache.has(bearerSrc)) return
|
|
84
|
+
let cancelled = false
|
|
85
|
+
fetchAsBlobUrl(bearerSrc, accept)
|
|
86
|
+
.catch(() => {})
|
|
87
|
+
.finally(() => {
|
|
88
|
+
if (!cancelled) rerender()
|
|
89
|
+
})
|
|
90
|
+
return () => {
|
|
91
|
+
cancelled = true
|
|
92
|
+
}
|
|
93
|
+
}, [bearerSrc, accept])
|
|
94
|
+
|
|
95
|
+
if (!src) return undefined
|
|
96
|
+
if (!bearerSrc) return src
|
|
97
|
+
return resolvedCache.get(bearerSrc)
|
|
98
|
+
}
|
|
@@ -1,93 +1,24 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { embedAuthedFetch, needsBearerAssetFetch } from '../utils/embed-authed-fetch'
|
|
3
|
+
import { clearAuthedAssetCache, useAuthedAssetSrc } from './use-authed-asset-src'
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* can't carry an `Authorization` header, so a gateway image URL that
|
|
11
|
-
* works on the cookie-auth web 401s in a shell. When
|
|
12
|
-
* `needsBearerAssetFetch` says the URL's origin is bearer-authed, this
|
|
13
|
-
* hook fetches it through `embedAuthedFetch` (bearer + deduped
|
|
14
|
-
* 401-refresh-retry + the adapter's cross-origin guard) and returns a
|
|
15
|
-
* blob object-URL; every other URL is returned unchanged, so on the
|
|
16
|
-
* cookie-auth web the hook is a pass-through.
|
|
6
|
+
* Image flavor of `useAuthedAssetSrc` — see that hook for the full contract.
|
|
7
|
+
* Kept as its own export because it is the lib's most-used consumer (avatars,
|
|
8
|
+
* entity images, markdown `<img>`) and because hosts already import this name.
|
|
17
9
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* Cache: module-level, session-lifetime, keyed by full URL. Gateway image
|
|
23
|
-
* URLs carry a `?v=<content-hash>` cache-buster, so a changed image is a
|
|
24
|
-
* new key; entries are never revoked (avatars are a few KB — refcount
|
|
25
|
-
* churn isn't worth it).
|
|
26
|
-
*
|
|
27
|
-
* Distinct from the legacy `useAuthenticatedImage`, which ALWAYS
|
|
28
|
-
* blob-fetches with `credentials: 'include'` and its own global config.
|
|
29
|
-
* This hook is pass-through-first and keys off the single auth knob the
|
|
30
|
-
* lib already has: the registered `EmbedAuthAdapter`.
|
|
10
|
+
* Distinct from the legacy `useAuthenticatedImage`, which ALWAYS blob-fetches
|
|
11
|
+
* with `credentials: 'include'` and its own global config. This hook is
|
|
12
|
+
* pass-through-first and keys off the single auth knob the lib already has:
|
|
13
|
+
* the registered `EmbedAuthAdapter`.
|
|
31
14
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let cacheGeneration = 0
|
|
15
|
+
export function useAuthedImageSrc(src?: string | null): string | undefined {
|
|
16
|
+
return useAuthedAssetSrc(src, 'image/*')
|
|
17
|
+
}
|
|
36
18
|
|
|
37
19
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* one's bearer. Fetches still in flight when the clear happens are fenced
|
|
42
|
-
* by a generation counter — their results are revoked instead of cached.
|
|
20
|
+
* Session-end cache drop. Now an alias of `clearAuthedAssetCache` — one cache
|
|
21
|
+
* backs every authed asset (images AND caption tracks), so hosts keep calling
|
|
22
|
+
* this single function on logout / forced re-login.
|
|
43
23
|
*/
|
|
44
|
-
export
|
|
45
|
-
cacheGeneration++
|
|
46
|
-
for (const url of resolvedCache.values()) URL.revokeObjectURL(url)
|
|
47
|
-
resolvedCache.clear()
|
|
48
|
-
inFlight.clear()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function fetchAsBlobUrl(src: string): Promise<void> {
|
|
52
|
-
let pending = inFlight.get(src)
|
|
53
|
-
if (!pending) {
|
|
54
|
-
const startedGeneration = cacheGeneration
|
|
55
|
-
pending = embedAuthedFetch(src, { headers: { Accept: 'image/*' } })
|
|
56
|
-
.then(async response => {
|
|
57
|
-
if (!response.ok) throw new Error(`image fetch failed: ${response.status}`)
|
|
58
|
-
const blobUrl = URL.createObjectURL(await response.blob())
|
|
59
|
-
if (startedGeneration === cacheGeneration) {
|
|
60
|
-
resolvedCache.set(src, blobUrl)
|
|
61
|
-
} else {
|
|
62
|
-
URL.revokeObjectURL(blobUrl)
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
.finally(() => {
|
|
66
|
-
if (inFlight.get(src) === pending) inFlight.delete(src)
|
|
67
|
-
})
|
|
68
|
-
inFlight.set(src, pending)
|
|
69
|
-
}
|
|
70
|
-
return pending
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function useAuthedImageSrc(src?: string | null): string | undefined {
|
|
74
|
-
const bearerSrc = src && needsBearerAssetFetch(src) ? src : null
|
|
75
|
-
const [, rerender] = useReducer((c: number) => c + 1, 0)
|
|
76
|
-
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
if (!bearerSrc || resolvedCache.has(bearerSrc)) return
|
|
79
|
-
let cancelled = false
|
|
80
|
-
fetchAsBlobUrl(bearerSrc)
|
|
81
|
-
.catch(() => {})
|
|
82
|
-
.finally(() => {
|
|
83
|
-
if (!cancelled) rerender()
|
|
84
|
-
})
|
|
85
|
-
return () => {
|
|
86
|
-
cancelled = true
|
|
87
|
-
}
|
|
88
|
-
}, [bearerSrc])
|
|
89
|
-
|
|
90
|
-
if (!src) return undefined
|
|
91
|
-
if (!bearerSrc) return src
|
|
92
|
-
return resolvedCache.get(bearerSrc)
|
|
93
|
-
}
|
|
24
|
+
export const clearAuthedImageCache = clearAuthedAssetCache
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { embedAuthedFetch, setEmbedAuthAdapter } from '../embed-authed-fetch'
|
|
2
|
+
import { embedAuthedFetch, needsBearerAssetFetch, setEmbedAuthAdapter } from '../embed-authed-fetch'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Tests focus on the same-origin guard rather than the proxy-auth
|
|
@@ -205,3 +205,52 @@ describe('embedAuthedFetch adapter.allowedOrigins (native-shell cross-origin san
|
|
|
205
205
|
expect(() => embedAuthedFetch('javascript:alert(1)')).toThrow(/non-http\(s\)/i)
|
|
206
206
|
})
|
|
207
207
|
})
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* `needsBearerAssetFetch` decides whether a `<img>` / `<track>` URL has to be
|
|
211
|
+
* blob-fetched (see `useAuthedAssetSrc`). The signal is the adapter's
|
|
212
|
+
* `Authorization` header — i.e. the host authenticates by HEADER, so nothing
|
|
213
|
+
* the browser fetches on its own carries auth, same-origin included.
|
|
214
|
+
*/
|
|
215
|
+
describe('needsBearerAssetFetch (native subresources that cannot carry a header)', () => {
|
|
216
|
+
const bearer = { getHeaders: () => ({ Authorization: 'Bearer t' }) }
|
|
217
|
+
|
|
218
|
+
afterEach(() => {
|
|
219
|
+
setEmbedAuthAdapter(null)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('is false with no adapter registered (the hub — cookies or nothing)', () => {
|
|
223
|
+
expect(needsBearerAssetFetch('/content/api/captions/release/1')).toBe(false)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('is false in cookie mode (adapter supplies no Authorization)', () => {
|
|
227
|
+
setEmbedAuthAdapter({ getHeaders: () => ({}) })
|
|
228
|
+
expect(needsBearerAssetFetch('/content/api/captions/release/1')).toBe(false)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('is TRUE for a relative proxy path in header-auth mode (dev-ticket web)', () => {
|
|
232
|
+
setEmbedAuthAdapter(bearer)
|
|
233
|
+
expect(needsBearerAssetFetch('/content/api/captions/release/1')).toBe(true)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('is TRUE for an absolute same-origin URL in header-auth mode', () => {
|
|
237
|
+
setEmbedAuthAdapter(bearer)
|
|
238
|
+
expect(needsBearerAssetFetch('http://localhost:3000/content/api/image-proxy?u=x')).toBe(true)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('is TRUE for a cross-origin gateway the adapter sanctions (native shell)', () => {
|
|
242
|
+
setEmbedAuthAdapter({ ...bearer, allowedOrigins: ['https://gateway.example'] })
|
|
243
|
+
expect(needsBearerAssetFetch('https://gateway.example/content/api/captions/release/1')).toBe(true)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('is false for a third-party origin the bearer does not belong to', () => {
|
|
247
|
+
setEmbedAuthAdapter({ ...bearer, allowedOrigins: ['https://gateway.example'] })
|
|
248
|
+
expect(needsBearerAssetFetch('https://cdn.example.com/pic.png')).toBe(false)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('is false for non-http(s) schemes (data:/blob: assets load natively)', () => {
|
|
252
|
+
setEmbedAuthAdapter(bearer)
|
|
253
|
+
expect(needsBearerAssetFetch('data:image/png;base64,AAAA')).toBe(false)
|
|
254
|
+
expect(needsBearerAssetFetch('blob:http://localhost:3000/abc')).toBe(false)
|
|
255
|
+
})
|
|
256
|
+
})
|
|
@@ -158,32 +158,49 @@ export function hasEmbedAuthAdapter(): boolean {
|
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
160
|
* Whether `url` is an asset the browser CANNOT load natively because its
|
|
161
|
-
* auth rides in request headers
|
|
162
|
-
* supplying an `Authorization` header AND the URL's origin is one the
|
|
163
|
-
* adapter explicitly sanctions for that bearer (`allowedOrigins` — the
|
|
164
|
-
* host's gateway, cross-origin by construction in native shells).
|
|
161
|
+
* auth rides in request headers.
|
|
165
162
|
*
|
|
166
|
-
* Native asset loads (`<img src>`, CSS `background-image`)
|
|
167
|
-
* custom headers, so a URL this returns `true` for must go
|
|
168
|
-
* `embedAuthedFetch` → blob object-URL instead (see
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
163
|
+
* Native asset loads (`<img src>`, `<track src>`, CSS `background-image`)
|
|
164
|
+
* can't carry custom headers, so a URL this returns `true` for must go
|
|
165
|
+
* through `embedAuthedFetch` → blob object-URL instead (see
|
|
166
|
+
* `useAuthedAssetSrc` / `useAuthedImageSrc`).
|
|
167
|
+
*
|
|
168
|
+
* The single signal is the registered adapter: when it supplies an
|
|
169
|
+
* `Authorization` header, the host authenticates by HEADER, so nothing the
|
|
170
|
+
* browser fetches on its own is authenticated. That holds for BOTH shapes a
|
|
171
|
+
* header-auth host takes:
|
|
172
|
+
*
|
|
173
|
+
* - a cross-origin gateway URL — sanctioned via `allowedOrigins` (native
|
|
174
|
+
* shells, whose page origin is `capacitor://` / `tauri://`);
|
|
175
|
+
* - a same-origin / relative reverse-proxy path (`/content/api/...`) —
|
|
176
|
+
* dev-ticket web, where the token lives in localStorage and there is NO
|
|
177
|
+
* session cookie to fall back on. This case used to be excluded on the
|
|
178
|
+
* assumption that same-origin implies "cookies work"; true for a cookie
|
|
179
|
+
* session, false for every header-auth host, which is exactly when this
|
|
180
|
+
* predicate is consulted at all.
|
|
181
|
+
*
|
|
182
|
+
* Everything else loads natively, unchanged: cookie-auth web (the adapter
|
|
183
|
+
* supplies no `Authorization`), no adapter at all (the hub), and third-party
|
|
184
|
+
* origins the bearer does NOT belong to (public images) — those stay out of
|
|
185
|
+
* reach of the token by the same origin rules `embedAuthedFetch` enforces.
|
|
172
186
|
*/
|
|
173
187
|
export function needsBearerAssetFetch(url: string): boolean {
|
|
174
188
|
if (typeof window === 'undefined') return false
|
|
175
189
|
const adapter = getRegisteredAuthAdapter()
|
|
176
|
-
if (!adapter
|
|
177
|
-
return false
|
|
178
|
-
}
|
|
190
|
+
if (!adapter || adapter.getHeaders?.().Authorization === undefined) return false
|
|
179
191
|
let target: URL
|
|
192
|
+
let pageOrigin: string
|
|
180
193
|
try {
|
|
181
194
|
target = new URL(url, window.location.href)
|
|
195
|
+
// Same reason `assertSameOrigin` derives it this way: test environments
|
|
196
|
+
// mock `window.location` as a plain object with no `origin` field.
|
|
197
|
+
pageOrigin = new URL(window.location.href).origin
|
|
182
198
|
} catch {
|
|
183
199
|
return false
|
|
184
200
|
}
|
|
185
201
|
if (target.protocol !== 'http:' && target.protocol !== 'https:') return false
|
|
186
|
-
|
|
202
|
+
if (target.origin === pageOrigin) return true
|
|
203
|
+
return adapter.allowedOrigins?.includes(target.origin) ?? false
|
|
187
204
|
}
|
|
188
205
|
|
|
189
206
|
/**
|