@flamingo-stack/openframe-frontend-core 0.0.853 → 0.0.855
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/build/export-map.json +1 -1
- package/dist/{case-study-card-WP5Z234Q.cjs → case-study-card-6VAMNHLU.cjs} +8 -8
- package/dist/{chunk-2W22LY3W.cjs → chunk-2JCA7BRK.cjs} +19 -19
- package/dist/{chunk-53QPG7SJ.cjs → chunk-2MCPK6AX.cjs} +28 -28
- package/dist/{chunk-QOW2GP3V.cjs → chunk-2P72KAG5.cjs} +23 -23
- package/dist/{chunk-NMWLXHFH.cjs → chunk-3GJ75OUL.cjs} +183 -183
- package/dist/{chunk-P63CRPWI.cjs → chunk-4H3J57AN.cjs} +25 -25
- package/dist/{chunk-6GWRPW6B.cjs → chunk-5NK3FUHY.cjs} +4 -4
- package/dist/{chunk-DLTPSP5F.cjs → chunk-5TYG5JNB.cjs} +2 -2
- package/dist/{chunk-EIACEY2L.cjs → chunk-65NMKAPQ.cjs} +12 -12
- package/dist/{chunk-IGVAPIEW.cjs → chunk-6C4OR4MZ.cjs} +2 -2
- package/dist/{chunk-GG466XFQ.cjs → chunk-6DMH4GXN.cjs} +30 -5
- package/dist/{chunk-EF2FZ7GT.cjs → chunk-6VVQJU76.cjs} +54 -54
- package/dist/{chunk-G7GF4PWA.cjs → chunk-77GX573I.cjs} +9 -9
- package/dist/{chunk-7RZHSBNX.cjs → chunk-AOOHJLXS.cjs} +90 -90
- package/dist/{chunk-O3EKOXFZ.cjs → chunk-AYMY2VNB.cjs} +37 -43
- package/dist/{chunk-67CZ2Q5K.cjs → chunk-BSPIGZCC.cjs} +36 -36
- package/dist/{chunk-FW7RKWPY.cjs → chunk-D6GEMVHD.cjs} +3 -3
- package/dist/{chunk-KEV6F3QN.cjs → chunk-DPNFI5XM.cjs} +100 -100
- package/dist/{chunk-6V2M2X3M.cjs → chunk-DTZ6KASI.cjs} +6 -6
- package/dist/{chunk-SSKMGH5O.cjs → chunk-DXVGGWLG.cjs} +10 -10
- package/dist/{chunk-IO6NN3SL.cjs → chunk-EOUEUYCQ.cjs} +54 -54
- package/dist/{chunk-5RMBLOJ5.cjs → chunk-ESGE7CUX.cjs} +14 -14
- package/dist/{chunk-ETZFBB7O.cjs → chunk-EXQSIX2I.cjs} +4 -4
- package/dist/{chunk-IC4IJJB2.cjs → chunk-F33EAPD2.cjs} +124 -124
- package/dist/{chunk-COWMXUHN.cjs → chunk-GK577ZLJ.cjs} +6 -14
- package/dist/{chunk-E4MFQKPC.cjs → chunk-GZYRDLJW.cjs} +103 -103
- package/dist/{chunk-LFEURIZN.cjs → chunk-HML3JUXF.cjs} +324 -322
- package/dist/{chunk-BNHTBLAG.cjs → chunk-J3CFRGZ2.cjs} +17 -17
- package/dist/{chunk-USPUZO53.cjs → chunk-KZAQPRTA.cjs} +9 -11
- package/dist/{chunk-HTYJUMFB.cjs → chunk-LSLRJJFP.cjs} +2 -2
- package/dist/{chunk-TBGQMHCG.cjs → chunk-MINFN3IG.cjs} +8 -8
- package/dist/{chunk-KHOB3TES.cjs → chunk-MX52LZ6M.cjs} +6 -6
- package/dist/{chunk-OH5TXFRR.cjs → chunk-O4CPT3RK.cjs} +7 -7
- package/dist/{chunk-HF3V6WBJ.cjs → chunk-OTHQ2KQW.cjs} +3 -3
- package/dist/{chunk-IUDBG3HJ.cjs → chunk-PCQ2HKU3.cjs} +5 -5
- package/dist/{chunk-BNG6JOGZ.cjs → chunk-QPHXYE5P.cjs} +27 -27
- package/dist/{chunk-UO2EJGRJ.cjs → chunk-QZXCUY36.cjs} +53 -53
- package/dist/{chunk-COJG2ATY.cjs → chunk-RIJU4HZ2.cjs} +51 -55
- package/dist/{chunk-KKFDRCQF.cjs → chunk-RY7INTYU.cjs} +14 -1
- package/dist/{chunk-GI3MMBP3.cjs → chunk-SQ7YHWEP.cjs} +228 -15
- package/dist/{chunk-U767P4JU.cjs → chunk-STV6VJ3U.cjs} +21 -21
- package/dist/chunk-T54I5ZZY.cjs +62 -0
- package/dist/{chunk-UV45EPXN.cjs → chunk-TW5Y4D5I.cjs} +7 -7
- package/dist/{chunk-FX35L4Y6.cjs → chunk-XCQ5I27Z.cjs} +3 -3
- package/dist/{chunk-AZEO7HBB.cjs → chunk-Y44EF4JW.cjs} +2 -2
- package/dist/{chunk-W7OFAROY.cjs → chunk-YSXVRTGT.cjs} +9 -12
- package/dist/{chunk-GI6ECCWB.cjs → chunk-ZUGJLCYP.cjs} +1272 -856
- package/dist/{chunk-KNN2GCMW.cjs → chunk-ZYAVIRQV.cjs} +25 -25
- package/dist/{cloud-tenants-QUL2XPAS.cjs → cloud-tenants-2XFIDDGU.cjs} +28 -29
- package/dist/components/case-studies/index.cjs +44 -45
- package/dist/components/chat/chat-message-row.d.ts +1 -1
- package/dist/components/chat/chat-message-row.d.ts.map +1 -1
- package/dist/components/chat/chat-message-row.js +4 -4
- package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts +7 -13
- package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/use-cover-image-fallback.js +2 -8
- package/dist/components/chat/hooks/use-chat-attachment-image-gallery.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-attachment-image-gallery.js +8 -4
- package/dist/components/chat/index.cjs +22 -23
- package/dist/components/contact/index.cjs +29 -30
- package/dist/components/docs/index.cjs +32 -33
- package/dist/components/embeds/index.cjs +29 -30
- package/dist/components/faq/index.cjs +30 -31
- package/dist/components/features/cloud-tenants/index.d.ts +7 -5
- package/dist/components/features/cloud-tenants/index.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/index.js +8 -1
- package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.d.ts +26 -0
- package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.d.ts.map +1 -0
- package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.js +176 -0
- package/dist/components/features/cloud-tenants/tenant-access-cell.d.ts +4 -2
- package/dist/components/features/cloud-tenants/tenant-access-cell.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-access-cell.js +3 -2
- package/dist/components/features/cloud-tenants/tenant-customer-cell.d.ts +12 -5
- package/dist/components/features/cloud-tenants/tenant-customer-cell.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-customer-cell.js +38 -7
- package/dist/components/features/cloud-tenants/tenant-licenses-cell.d.ts +7 -0
- package/dist/components/features/cloud-tenants/tenant-licenses-cell.d.ts.map +1 -0
- package/dist/components/features/cloud-tenants/tenant-licenses-cell.js +17 -0
- package/dist/components/features/cloud-tenants/tenant-presentation.d.ts +19 -0
- package/dist/components/features/cloud-tenants/tenant-presentation.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-presentation.js +24 -3
- package/dist/components/features/cloud-tenants/tenant-row.d.ts +35 -8
- package/dist/components/features/cloud-tenants/tenant-row.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenants-table-columns.d.ts +9 -3
- package/dist/components/features/cloud-tenants/tenants-table-columns.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenants-table-columns.js +5 -3
- package/dist/components/features/cloud-tenants/tenants-table-view.d.ts +27 -6
- package/dist/components/features/cloud-tenants/tenants-table-view.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenants-table-view.js +75 -17
- package/dist/components/features/devices/device-directory.d.ts +18 -0
- package/dist/components/features/devices/device-directory.d.ts.map +1 -0
- package/dist/components/features/devices/device-directory.js +35 -0
- package/dist/components/features/devices/devices-filter-toolbar.d.ts +3 -1
- package/dist/components/features/devices/devices-filter-toolbar.d.ts.map +1 -1
- package/dist/components/features/devices/devices-filter-toolbar.js +2 -1
- package/dist/components/features/devices/devices-panel-view.d.ts +3 -1
- package/dist/components/features/devices/devices-panel-view.d.ts.map +1 -1
- package/dist/components/features/devices/devices-panel-view.js +11 -2
- package/dist/components/features/devices/devices-table-columns.d.ts +17 -3
- package/dist/components/features/devices/devices-table-columns.d.ts.map +1 -1
- package/dist/components/features/devices/devices-table-columns.js +80 -10
- package/dist/components/features/devices/index.d.ts +1 -0
- package/dist/components/features/devices/index.d.ts.map +1 -1
- package/dist/components/features/devices/index.js +8 -0
- package/dist/components/features/devices/types.d.ts +14 -0
- package/dist/components/features/devices/types.d.ts.map +1 -1
- package/dist/components/features/devices/use-tag-filter-modal.d.ts +1 -0
- package/dist/components/features/devices/use-tag-filter-modal.d.ts.map +1 -1
- package/dist/components/features/devices/use-tag-filter-modal.js +13 -8
- package/dist/components/features/index.cjs +42 -29
- package/dist/components/help-center-pages/index.cjs +134 -135
- package/dist/components/index.cjs +126 -113
- package/dist/components/meeting-scheduler/index.cjs +102 -103
- package/dist/components/navigation/app-layout.js +2 -2
- package/dist/components/navigation/index.cjs +31 -32
- package/dist/components/onboarding-guides/index.cjs +31 -32
- package/dist/components/product-demo/index.cjs +39 -40
- package/dist/components/related-content/index.cjs +16 -17
- package/dist/components/tickets/index.cjs +35 -36
- package/dist/components/ui/date-picker.d.ts +2 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -1
- package/dist/components/ui/date-picker.js +19 -6
- package/dist/components/ui/entity-image.d.ts +1 -2
- package/dist/components/ui/entity-image.d.ts.map +1 -1
- package/dist/components/ui/entity-image.js +4 -8
- package/dist/components/ui/index.cjs +28 -29
- package/dist/components/ui/markdown/base-components.d.ts +0 -13
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -1
- package/dist/components/ui/markdown/base-components.js +7 -7
- package/dist/components/ui/square-avatar.d.ts.map +1 -1
- package/dist/components/ui/square-avatar.js +3 -8
- package/dist/{devices-LNB4CYPP.cjs → devices-O3DYGIZB.cjs} +29 -30
- package/dist/embed-shims/index.cjs +3 -2
- package/dist/embed-shims/next-image.cjs +3 -2
- package/dist/embed-shims/next-image.d.ts +2 -0
- package/dist/embed-shims/next-image.d.ts.map +1 -1
- package/dist/embed-shims/next-image.js +14 -4
- package/dist/{embeddable-chat-SJEEJI4N.cjs → embeddable-chat-QD7NFD2H.cjs} +100 -101
- package/dist/hooks/index.cjs +27 -16
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/ui/use-content-breakpoint.d.ts +8 -0
- package/dist/hooks/ui/use-content-breakpoint.d.ts.map +1 -1
- package/dist/hooks/ui/use-content-breakpoint.js +17 -3
- package/dist/hooks/ui/use-media-query.d.ts +1 -0
- package/dist/hooks/ui/use-media-query.d.ts.map +1 -1
- package/dist/hooks/ui/use-media-query.js +3 -1
- package/dist/hooks/use-authed-asset-src.d.ts +77 -2
- package/dist/hooks/use-authed-asset-src.d.ts.map +1 -1
- package/dist/hooks/use-authed-asset-src.js +185 -73
- package/dist/hooks/use-authed-image-src.d.ts +11 -4
- package/dist/hooks/use-authed-image-src.d.ts.map +1 -1
- package/dist/hooks/use-authed-image-src.js +6 -2
- package/dist/hooks/use-image-fallback.d.ts +16 -0
- package/dist/hooks/use-image-fallback.d.ts.map +1 -0
- package/dist/hooks/use-image-fallback.js +14 -0
- package/dist/index.cjs +58 -36
- package/dist/{knowledge-UTWKB6XG.cjs → knowledge-K7AFNZNO.cjs} +29 -30
- package/dist/{logs-PMXIWMBQ.cjs → logs-MZ3BR2FR.cjs} +35 -36
- package/dist/{policies-6DCMFDL7.cjs → policies-XVKQ2W7S.cjs} +32 -33
- package/dist/{remote-session-SG44CNOK.cjs → remote-session-VOZJXDM4.cjs} +13 -14
- package/dist/{scripts-2G7LXW7Z.cjs → scripts-IBPMBS6G.cjs} +35 -36
- package/dist/{software-update-MYLKHDHC.cjs → software-update-PXTANMAO.cjs} +32 -33
- package/dist/{tickets-board-SQY2Y6QV.cjs → tickets-board-POYZS7BW.cjs} +16 -17
- package/dist/utils/app-origins.d.ts +3 -6
- package/dist/utils/app-origins.d.ts.map +1 -1
- package/dist/utils/app-origins.js +8 -1
- package/dist/utils/embed-authed-fetch.d.ts +46 -26
- package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
- package/dist/utils/embed-authed-fetch.js +24 -8
- package/dist/utils/index.cjs +4 -1
- package/dist/utils/presentation-for.d.ts +6 -0
- package/dist/utils/presentation-for.d.ts.map +1 -0
- package/dist/utils/presentation-for.js +7 -0
- package/dist/{video-file-player-XMOB234V.cjs → video-file-player-RIOT24NT.cjs} +3 -4
- package/package.json +1 -1
- package/src/components/chat/chat-message-row.tsx +8 -7
- package/src/components/chat/entity-cards/use-cover-image-fallback.ts +8 -28
- package/src/components/chat/hooks/use-chat-attachment-image-gallery.tsx +19 -8
- package/src/components/features/cloud-tenants/index.ts +8 -3
- package/src/components/features/cloud-tenants/single-customer-tenants-table-view.tsx +234 -0
- package/src/components/features/cloud-tenants/tenant-access-cell.tsx +12 -5
- package/src/components/features/cloud-tenants/tenant-customer-cell.tsx +48 -12
- package/src/components/features/cloud-tenants/tenant-licenses-cell.tsx +28 -0
- package/src/components/features/cloud-tenants/tenant-presentation.ts +46 -4
- package/src/components/features/cloud-tenants/tenant-row.ts +38 -8
- package/src/components/features/cloud-tenants/tenants-table-columns.ts +8 -5
- package/src/components/features/cloud-tenants/tenants-table-view.tsx +123 -28
- package/src/components/features/devices/device-directory.ts +55 -0
- package/src/components/features/devices/devices-filter-toolbar.tsx +4 -1
- package/src/components/features/devices/devices-panel-view.tsx +12 -2
- package/src/components/features/devices/devices-table-columns.tsx +123 -8
- package/src/components/features/devices/index.ts +7 -0
- package/src/components/features/devices/types.ts +14 -0
- package/src/components/features/devices/use-tag-filter-modal.ts +32 -14
- package/src/components/navigation/app-layout.tsx +4 -4
- package/src/components/ui/date-picker.tsx +21 -3
- package/src/components/ui/entity-image.tsx +4 -12
- package/src/components/ui/markdown/base-components.tsx +15 -20
- package/src/components/ui/square-avatar.tsx +5 -8
- package/src/embed-shims/next-image.tsx +34 -3
- package/src/hooks/index.ts +1 -0
- package/src/hooks/ui/use-content-breakpoint.ts +21 -2
- package/src/hooks/ui/use-media-query.ts +1 -0
- package/src/hooks/use-authed-asset-src.ts +323 -107
- package/src/hooks/use-authed-image-src.ts +15 -5
- package/src/hooks/use-image-fallback.ts +31 -0
- package/src/utils/app-origins.ts +12 -7
- package/src/utils/embed-authed-fetch.ts +71 -32
- package/src/utils/presentation-for.ts +10 -0
- package/dist/chunk-UAI4VMEF.cjs +0 -46
- package/dist/chunk-YSDB6HGQ.cjs +0 -102
|
@@ -1,115 +1,326 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect,
|
|
4
|
-
import {
|
|
3
|
+
import { useEffect, useSyncExternalStore } from 'react';
|
|
4
|
+
import { subscribeRegisteredAdapter } from '../utils/embed-auth-adapter-registry';
|
|
5
|
+
import {
|
|
6
|
+
answeredAfterRefresh,
|
|
7
|
+
assetLoadMode,
|
|
8
|
+
embedAuthedFetch,
|
|
9
|
+
resolveAssetUrl,
|
|
10
|
+
subscribeEmbedCredentialChange,
|
|
11
|
+
} from '../utils/embed-authed-fetch';
|
|
5
12
|
import { useIsomorphicLayoutEffect } from './ui/use-isomorphic-layout-effect';
|
|
6
13
|
|
|
7
14
|
/**
|
|
8
15
|
* Resolve ANY asset URL the browser would load natively — `<img src>`,
|
|
9
|
-
* `<track src>`, a font, a poster —
|
|
10
|
-
* HEADERS instead of cookies: native shells (`capacitor://`, `tauri://`) and
|
|
11
|
-
* dev-ticket web, both in bearer mode. A native subresource load can't carry
|
|
12
|
-
* an `Authorization` header, so a gateway URL that works on the cookie-auth
|
|
13
|
-
* web 401s there.
|
|
16
|
+
* `<track src>`, a font, a poster — on every host:
|
|
14
17
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* 1. The host's `resolveAssetUrl` says where the URL lives, so a backend path
|
|
19
|
+
* the page's origin does not serve is drawn from the gateway.
|
|
20
|
+
* 2. Where auth rides in request HEADERS instead of cookies (native shells,
|
|
21
|
+
* `capacitor://` / `tauri://`, and dev-ticket web, both in bearer mode), a
|
|
22
|
+
* native subresource load can't carry the `Authorization` header and gets
|
|
23
|
+
* a 401. Such a URL is fetched through `embedAuthedFetch` (bearer + deduped
|
|
24
|
+
* 401-refresh-retry + the adapter's cross-origin guard) and drawn from a
|
|
25
|
+
* blob object-URL. Every other URL is drawn as it stands, so on the
|
|
26
|
+
* cookie-auth web this is a pass-through.
|
|
20
27
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* broken asset; a failed fetch retries on the next mount.
|
|
28
|
+
* The state lives in ONE module store read through `useSyncExternalStore`, so
|
|
29
|
+
* every mounted consumer follows it, memoized or not:
|
|
24
30
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* - A shell renders before its tokens arrive. A gateway URL is then `pending`
|
|
32
|
+
* (nothing is drawn, nothing is requested) and is fetched as soon as the
|
|
33
|
+
* adapter reports a credential (`subscribeEmbedCredentialChange`), a new
|
|
34
|
+
* adapter registers, or a consumer renders and finds one. It used to be
|
|
35
|
+
* handed to a native `<img>` at that first render, which got a 401 and was
|
|
36
|
+
* never asked again.
|
|
37
|
+
* - `failed` is its own state, so a caller can show its fallback at once, and
|
|
38
|
+
* it stays `failed` while a retry runs. What is retried, and when:
|
|
39
|
+
* - no answer at all (offline, a refused redirect): on the next credential
|
|
40
|
+
* and when the browser comes back online;
|
|
41
|
+
* - a 401 that `embedAuthedFetch` could not refresh its way past (no new
|
|
42
|
+
* credential came of it): on the next credential;
|
|
43
|
+
* - anything else, including a 401 to the retry `embedAuthedFetch` sent
|
|
44
|
+
* with a credential it had just refreshed (`answeredAfterRefresh`): only
|
|
45
|
+
* when a consumer mounts again after every one let go. A new credential
|
|
46
|
+
* cannot lift that refusal, and retrying it on each rotation would rotate
|
|
47
|
+
* again: two such pictures would keep rotating the session between them.
|
|
48
|
+
* - `clearAuthedAssetCache` (session end) drops every blob; consumers still
|
|
49
|
+
* mounted go back to `pending` and fetch again with whatever credential the
|
|
50
|
+
* host holds when one next asks (see there).
|
|
51
|
+
* - A 2xx `text/html` answer is a failure, not a picture: a native shell's
|
|
52
|
+
* asset server answers an unknown path with its SPA page.
|
|
53
|
+
*
|
|
54
|
+
* Reading the state through the store also keeps it correct under the React
|
|
55
|
+
* Compiler: a module map read during render is memoized on its key, so a hook
|
|
56
|
+
* that read `cache.get(src)` there would keep the `undefined` it saw before the
|
|
57
|
+
* fetch finished.
|
|
58
|
+
*
|
|
59
|
+
* Cache: keyed by the resolved URL, so the same avatar in twenty rows costs one
|
|
60
|
+
* request. Gateway URLs carry a `?v=<content-hash>` cache-buster, so changed
|
|
61
|
+
* content is a new key. Entries are reference-counted by the mounted consumers
|
|
62
|
+
* and revoked RELEASE_DELAY_MS after the last one unmounts (a remount inside
|
|
63
|
+
* that window keeps the blob): in a desktop shell open for days, every distinct
|
|
64
|
+
* picture ever shown would otherwise stay in memory.
|
|
34
65
|
*/
|
|
35
66
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
67
|
+
/** What a consumer draws, and why. */
|
|
68
|
+
export type AuthedAssetStatus =
|
|
69
|
+
/** Draw `src` as it stands: the browser loads it itself. */
|
|
70
|
+
| 'native'
|
|
71
|
+
/** Nothing to draw yet: waiting for a credential, or the first authed fetch is in flight. */
|
|
72
|
+
| 'pending'
|
|
73
|
+
/** `src` is the fetched blob. */
|
|
74
|
+
| 'ready'
|
|
75
|
+
/** The authed fetch failed (a retry may be running); `url` is what was asked for. */
|
|
76
|
+
| 'failed';
|
|
77
|
+
|
|
78
|
+
export interface AuthedAsset {
|
|
79
|
+
/** What to draw: the URL (`native`), its blob (`ready`), or nothing (`pending`, `failed`, or no URL given). */
|
|
80
|
+
readonly src: string | undefined;
|
|
81
|
+
/** The URL after the host's `resolveAssetUrl`; `undefined` when none was given. */
|
|
82
|
+
readonly url: string | undefined;
|
|
83
|
+
readonly status: AuthedAssetStatus;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** What makes a failed entry worth asking again (see the header). */
|
|
87
|
+
type RetryOn = 'credential-or-online' | 'credential' | 'remount';
|
|
88
|
+
|
|
89
|
+
type EntryState =
|
|
90
|
+
| { readonly status: 'idle' }
|
|
91
|
+
| { readonly status: 'loading'; readonly afterFailure: boolean }
|
|
92
|
+
| { readonly status: 'ready'; readonly blobUrl: string }
|
|
93
|
+
| { readonly status: 'failed'; readonly retryOn: RetryOn };
|
|
94
|
+
|
|
95
|
+
interface Entry {
|
|
96
|
+
state: EntryState;
|
|
97
|
+
accept: string;
|
|
98
|
+
refs: number;
|
|
99
|
+
releaseTimer: ReturnType<typeof setTimeout> | undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Why an entry is being looked at:
|
|
104
|
+
* - `join`: another consumer joining, or a consumer rendering while it waits. Starts only an idle entry.
|
|
105
|
+
* - `mount`: the first consumer back after every one let go. Also retries any failure.
|
|
106
|
+
* - `credential`: the adapter's credential changed, or a new adapter registered.
|
|
107
|
+
* - `online`: the browser came back online.
|
|
108
|
+
*/
|
|
109
|
+
type Trigger = 'join' | 'mount' | 'credential' | 'online';
|
|
39
110
|
|
|
40
111
|
/** How long an unused blob is kept before it is revoked, in ms. */
|
|
41
112
|
const RELEASE_DELAY_MS = 60_000;
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
113
|
+
|
|
114
|
+
const IDLE: EntryState = { status: 'idle' };
|
|
115
|
+
|
|
116
|
+
/** A non-2xx answer, or a page where an asset was asked for. */
|
|
117
|
+
class AssetResponseError extends Error {
|
|
118
|
+
readonly status: number;
|
|
119
|
+
readonly afterRefresh: boolean;
|
|
120
|
+
|
|
121
|
+
constructor(response: Response) {
|
|
122
|
+
super(`asset fetch failed: ${response.status}`);
|
|
123
|
+
this.status = response.status;
|
|
124
|
+
this.afterRefresh = answeredAfterRefresh(response);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const entries = new Map<string, Entry>();
|
|
129
|
+
const listeners = new Set<() => void>();
|
|
130
|
+
/** Moves on `clearAuthedAssetCache`: fences fetches started under the previous session. */
|
|
131
|
+
let cacheGeneration = 0;
|
|
132
|
+
let notifyScheduled = false;
|
|
133
|
+
let detachSources: (() => void) | null = null;
|
|
134
|
+
|
|
135
|
+
function notify(): void {
|
|
136
|
+
if (notifyScheduled) return;
|
|
137
|
+
notifyScheduled = true;
|
|
138
|
+
// One pass per burst: a credential change can settle dozens of entries at once.
|
|
139
|
+
queueMicrotask(() => {
|
|
140
|
+
notifyScheduled = false;
|
|
141
|
+
for (const listener of [...listeners]) listener();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Settle `url`'s entry, unless it was dropped or the cache cleared since `entry` was read. */
|
|
146
|
+
function settle(url: string, entry: Entry, generation: number, state: EntryState): boolean {
|
|
147
|
+
const current = entries.get(url);
|
|
148
|
+
if (generation !== cacheGeneration || current !== entry) return false;
|
|
149
|
+
current.state = state;
|
|
150
|
+
notify();
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function due(state: EntryState, trigger: Trigger): boolean {
|
|
155
|
+
if (state.status === 'idle') return true;
|
|
156
|
+
if (state.status !== 'failed') return false;
|
|
157
|
+
switch (trigger) {
|
|
158
|
+
case 'mount':
|
|
159
|
+
return true;
|
|
160
|
+
case 'credential':
|
|
161
|
+
return state.retryOn !== 'remount';
|
|
162
|
+
case 'online':
|
|
163
|
+
return state.retryOn === 'credential-or-online';
|
|
164
|
+
default:
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function retryOnFor(error: unknown): RetryOn {
|
|
170
|
+
// `fetch` rejects with a TypeError when the request got no answer at all: offline, DNS, a refused redirect.
|
|
171
|
+
if (error instanceof TypeError) return 'credential-or-online';
|
|
172
|
+
if (error instanceof AssetResponseError && error.status === 401 && !error.afterRefresh) return 'credential';
|
|
173
|
+
return 'remount';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Start the authed fetch when `trigger` makes the entry due and a credential is there to send. */
|
|
177
|
+
function kick(url: string, trigger: Trigger): void {
|
|
178
|
+
const entry = entries.get(url);
|
|
179
|
+
if (!entry || !due(entry.state, trigger) || assetLoadMode(url) !== 'bearer') return;
|
|
180
|
+
const generation = cacheGeneration;
|
|
181
|
+
settle(url, entry, generation, { status: 'loading', afterFailure: entry.state.status === 'failed' });
|
|
182
|
+
// `embedAuthedFetch` throws synchronously on a URL its origin guard refuses: one failure path for both.
|
|
183
|
+
Promise.resolve()
|
|
184
|
+
.then(() => embedAuthedFetch(url, { headers: { Accept: entry.accept } }))
|
|
185
|
+
.then(async response => {
|
|
186
|
+
if (!response.ok || /^\s*text\/html\b/i.test(response.headers.get('Content-Type') ?? '')) {
|
|
187
|
+
throw new AssetResponseError(response);
|
|
188
|
+
}
|
|
189
|
+
return URL.createObjectURL(await response.blob());
|
|
190
|
+
})
|
|
191
|
+
.then(
|
|
192
|
+
blobUrl => {
|
|
193
|
+
if (!settle(url, entry, generation, { status: 'ready', blobUrl })) URL.revokeObjectURL(blobUrl);
|
|
194
|
+
},
|
|
195
|
+
(error: unknown) => {
|
|
196
|
+
settle(url, entry, generation, { status: 'failed', retryOn: retryOnFor(error) });
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function sweep(trigger: Trigger): void {
|
|
202
|
+
for (const [url, entry] of entries) if (entry.refs > 0) kick(url, trigger);
|
|
203
|
+
// Even with nothing to fetch, a URL that was awaiting a credential may be classified differently now.
|
|
204
|
+
notify();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function onCredentialChange(): void {
|
|
208
|
+
sweep('credential');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function onOnline(): void {
|
|
212
|
+
sweep('online');
|
|
57
213
|
}
|
|
58
214
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
215
|
+
/** Listen for what can make a waiting or failed entry loadable: a new adapter, a new credential, the network. */
|
|
216
|
+
function attachSources(): () => void {
|
|
217
|
+
let detachCredential = subscribeEmbedCredentialChange(onCredentialChange);
|
|
218
|
+
const detachAdapter = subscribeRegisteredAdapter(() => {
|
|
219
|
+
// `subscribeEmbedCredentialChange` binds to the adapter registered when it is called.
|
|
220
|
+
detachCredential();
|
|
221
|
+
detachCredential = subscribeEmbedCredentialChange(onCredentialChange);
|
|
222
|
+
onCredentialChange();
|
|
223
|
+
});
|
|
224
|
+
window.addEventListener('online', onOnline);
|
|
225
|
+
return () => {
|
|
226
|
+
detachCredential();
|
|
227
|
+
detachAdapter();
|
|
228
|
+
window.removeEventListener('online', onOnline);
|
|
229
|
+
};
|
|
63
230
|
}
|
|
64
231
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
232
|
+
/** No URL, nothing that can change: the many pictures drawn without one stay off the store. */
|
|
233
|
+
function subscribeNothing(): () => void {
|
|
234
|
+
return () => {};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function subscribe(listener: () => void): () => void {
|
|
238
|
+
listeners.add(listener);
|
|
239
|
+
detachSources ??= attachSources();
|
|
240
|
+
return () => {
|
|
241
|
+
listeners.delete(listener);
|
|
242
|
+
if (listeners.size === 0 && detachSources) {
|
|
243
|
+
detachSources();
|
|
244
|
+
detachSources = null;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function acquire(url: string, accept: string): void {
|
|
250
|
+
let entry = entries.get(url);
|
|
251
|
+
if (!entry) {
|
|
252
|
+
entry = { state: IDLE, accept, refs: 0, releaseTimer: undefined };
|
|
253
|
+
entries.set(url, entry);
|
|
70
254
|
}
|
|
71
|
-
|
|
72
|
-
|
|
255
|
+
clearTimeout(entry.releaseTimer);
|
|
256
|
+
entry.releaseTimer = undefined;
|
|
257
|
+
entry.refs++;
|
|
258
|
+
kick(url, entry.refs === 1 ? 'mount' : 'join');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function release(url: string): void {
|
|
262
|
+
const entry = entries.get(url);
|
|
263
|
+
if (!entry) return;
|
|
264
|
+
entry.refs = Math.max(0, entry.refs - 1);
|
|
265
|
+
if (entry.refs > 0) return;
|
|
266
|
+
clearTimeout(entry.releaseTimer);
|
|
267
|
+
entry.releaseTimer = setTimeout(() => {
|
|
268
|
+
if (entries.get(url) !== entry || entry.refs > 0) return;
|
|
269
|
+
if (entry.state.status === 'ready') URL.revokeObjectURL(entry.state.blobUrl);
|
|
270
|
+
entries.delete(url);
|
|
271
|
+
}, RELEASE_DELAY_MS);
|
|
73
272
|
}
|
|
74
273
|
|
|
75
274
|
/**
|
|
76
275
|
* Drop every cached blob and revoke its object URL. Hosts call this at
|
|
77
276
|
* session end (logout / forced re-login) so a follow-on login as a
|
|
78
277
|
* different identity cannot be served blobs fetched under the previous
|
|
79
|
-
* one's bearer. Fetches still in flight
|
|
80
|
-
*
|
|
278
|
+
* one's bearer. Fetches still in flight are fenced by a generation counter
|
|
279
|
+
* (their results are revoked instead of cached). Consumers still mounted go
|
|
280
|
+
* back to `pending`; the clear itself starts no fetch, so a host may clear
|
|
281
|
+
* just before it drops its credential, in the same task. A consumer that
|
|
282
|
+
* looks again later (a credential change, a mount, a render) fetches with
|
|
283
|
+
* whatever credential the host holds then.
|
|
81
284
|
*/
|
|
82
285
|
export function clearAuthedAssetCache(): void {
|
|
83
286
|
cacheGeneration++;
|
|
84
|
-
for (const url of
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
function fetchAsBlobUrl(src: string, accept: string): Promise<void> {
|
|
92
|
-
let pending = inFlight.get(src);
|
|
93
|
-
if (!pending) {
|
|
94
|
-
const startedGeneration = cacheGeneration;
|
|
95
|
-
pending = embedAuthedFetch(src, { headers: { Accept: accept } })
|
|
96
|
-
.then(async response => {
|
|
97
|
-
if (!response.ok) throw new Error(`asset fetch failed: ${response.status}`);
|
|
98
|
-
const blobUrl = URL.createObjectURL(await response.blob());
|
|
99
|
-
if (startedGeneration === cacheGeneration) {
|
|
100
|
-
resolvedCache.set(src, blobUrl);
|
|
101
|
-
// Everyone who asked for it unmounted while it was loading.
|
|
102
|
-
if (!refCounts.has(src)) scheduleRelease(src);
|
|
103
|
-
} else {
|
|
104
|
-
URL.revokeObjectURL(blobUrl);
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
.finally(() => {
|
|
108
|
-
if (inFlight.get(src) === pending) inFlight.delete(src);
|
|
109
|
-
});
|
|
110
|
-
inFlight.set(src, pending);
|
|
287
|
+
for (const [url, entry] of entries) {
|
|
288
|
+
if (entry.state.status === 'ready') URL.revokeObjectURL(entry.state.blobUrl);
|
|
289
|
+
clearTimeout(entry.releaseTimer);
|
|
290
|
+
entry.releaseTimer = undefined;
|
|
291
|
+
if (entry.refs > 0) entry.state = IDLE;
|
|
292
|
+
else entries.delete(url);
|
|
111
293
|
}
|
|
112
|
-
|
|
294
|
+
notify();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const NO_ASSET: AuthedAsset = Object.freeze({ src: undefined, url: undefined, status: 'native' });
|
|
298
|
+
|
|
299
|
+
function readAsset(src: string | null | undefined): AuthedAsset {
|
|
300
|
+
if (!src) return NO_ASSET;
|
|
301
|
+
const url = resolveAssetUrl(src);
|
|
302
|
+
const mode = assetLoadMode(url);
|
|
303
|
+
if (mode === 'native') return { src: url, url, status: 'native' };
|
|
304
|
+
const state = mode === 'bearer' ? entries.get(url)?.state : undefined;
|
|
305
|
+
if (state?.status === 'ready') return { src: state.blobUrl, url, status: 'ready' };
|
|
306
|
+
if (state?.status === 'failed' || (state?.status === 'loading' && state.afterFailure)) {
|
|
307
|
+
return { src: undefined, url, status: 'failed' };
|
|
308
|
+
}
|
|
309
|
+
return { src: undefined, url, status: 'pending' };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The store's answer as one string: `useSyncExternalStore` compares snapshots
|
|
314
|
+
* by identity, and a string is equal by value, with no cache of answer objects
|
|
315
|
+
* to grow and evict.
|
|
316
|
+
*/
|
|
317
|
+
function encode(asset: AuthedAsset): string {
|
|
318
|
+
return JSON.stringify([asset.status, asset.url ?? null, asset.src ?? null]);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function decode(snapshot: string): AuthedAsset {
|
|
322
|
+
const [status, url, src] = JSON.parse(snapshot) as [AuthedAssetStatus, string | null, string | null];
|
|
323
|
+
return { status, url: url ?? undefined, src: src ?? undefined };
|
|
113
324
|
}
|
|
114
325
|
|
|
115
326
|
/**
|
|
@@ -118,33 +329,38 @@ function fetchAsBlobUrl(src: string, accept: string): Promise<void> {
|
|
|
118
329
|
* consumer renders (`image/*`, `text/vtt`); it also replaces
|
|
119
330
|
* `embedAuthedFetch`'s default JSON `Content-Type` on the GET.
|
|
120
331
|
*/
|
|
121
|
-
export function
|
|
122
|
-
const
|
|
123
|
-
|
|
332
|
+
export function useAuthedAsset(src?: string | null, accept = '*/*'): AuthedAsset {
|
|
333
|
+
const snapshot = useSyncExternalStore(
|
|
334
|
+
src ? subscribe : subscribeNothing,
|
|
335
|
+
() => encode(readAsset(src)),
|
|
336
|
+
// The server has no adapter: it draws the URL as given, and the client takes over after hydration.
|
|
337
|
+
() => encode(src ? { src, url: src, status: 'native' } : NO_ASSET),
|
|
338
|
+
);
|
|
339
|
+
const asset = decode(snapshot);
|
|
340
|
+
const tracked = asset.status === 'native' ? undefined : asset.url;
|
|
124
341
|
|
|
125
|
-
// Layout, not passive: this render
|
|
342
|
+
// Layout, not passive: this render may hand out a cached blob URL, and a
|
|
126
343
|
// release timer firing before a passive effect ran would revoke it under
|
|
127
|
-
// the committed
|
|
344
|
+
// the committed element.
|
|
128
345
|
useIsomorphicLayoutEffect(() => {
|
|
129
|
-
if (!
|
|
130
|
-
acquire(
|
|
131
|
-
return () => release(
|
|
132
|
-
}, [
|
|
346
|
+
if (!tracked) return undefined;
|
|
347
|
+
acquire(tracked, accept);
|
|
348
|
+
return () => release(tracked);
|
|
349
|
+
}, [tracked, accept]);
|
|
133
350
|
|
|
351
|
+
// A consumer that renders while it waits looks again, because a credential can be there with nobody having told
|
|
352
|
+
// the store: it arrived between this consumer's acquire (layout effect) and its subscribe (passive effect), or the
|
|
353
|
+
// adapter never announced it (another tab's sign-in reaches this one only through `storage`). Starts only an idle
|
|
354
|
+
// entry.
|
|
355
|
+
const waiting = tracked !== undefined && asset.status === 'pending';
|
|
134
356
|
useEffect(() => {
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
};
|
|
145
|
-
}, [bearerSrc, accept]);
|
|
146
|
-
|
|
147
|
-
if (!src) return undefined;
|
|
148
|
-
if (!bearerSrc) return src;
|
|
149
|
-
return resolvedCache.get(bearerSrc);
|
|
357
|
+
if (waiting) kick(tracked, 'join');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return asset;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** `useAuthedAsset`'s `src`: what to draw, `undefined` while there is nothing to draw. */
|
|
364
|
+
export function useAuthedAssetSrc(src?: string | null, accept = '*/*'): string | undefined {
|
|
365
|
+
return useAuthedAsset(src, accept).src;
|
|
150
366
|
}
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { clearAuthedAssetCache,
|
|
3
|
+
import { type AuthedAsset, clearAuthedAssetCache, useAuthedAsset } from './use-authed-asset-src';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Image flavor of `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Image flavor of `useAuthedAsset` — see that hook for the full contract. The
|
|
7
|
+
* one place a picture's `Accept` is stated: a store entry keeps the `Accept` of
|
|
8
|
+
* whichever consumer acquired it first, so the boxed `Image` and every picture
|
|
9
|
+
* component around it must ask the same.
|
|
9
10
|
*
|
|
10
11
|
* Distinct from the legacy `useAuthenticatedImage`, which ALWAYS blob-fetches
|
|
11
12
|
* with `credentials: 'include'` and its own global config. This hook is
|
|
12
13
|
* pass-through-first and keys off the single auth knob the lib already has:
|
|
13
14
|
* the registered `EmbedAuthAdapter`.
|
|
14
15
|
*/
|
|
16
|
+
export function useAuthedImage(src?: string | null): AuthedAsset {
|
|
17
|
+
return useAuthedAsset(src, 'image/*');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `useAuthedImage`'s `src`. Kept as its own export because it is the lib's
|
|
22
|
+
* most-used consumer (avatars, entity images, markdown `<img>`) and because
|
|
23
|
+
* hosts already import this name.
|
|
24
|
+
*/
|
|
15
25
|
export function useAuthedImageSrc(src?: string | null): string | undefined {
|
|
16
|
-
return
|
|
26
|
+
return useAuthedImage(src).src;
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
/**
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface ImageFallback {
|
|
6
|
+
/** The first candidate that has not failed, else null: draw your own fallback (initials, a placeholder, nothing). */
|
|
7
|
+
src: string | null;
|
|
8
|
+
/** Wire to the picture's `onError`. */
|
|
9
|
+
onError: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A picture's fallback chain, keyed by URL: how a component falls back from a picture that did not load
|
|
14
|
+
* (`SquareAvatar`, `EntityImage`, `ChatMessageRow`, the entity cards' covers). State is the SET of URLs that failed
|
|
15
|
+
* and `src` is derived, so a new URL is simply not one of them: the picture comes back with no reset effect, and a
|
|
16
|
+
* URL that failed once is not retried in a loop. Never hide the element or reveal a fallback by writing to the DOM
|
|
17
|
+
* in `onError` instead (the legacy `hideOnError`): React does not undo the write, so the fallback outlives the URL
|
|
18
|
+
* it was about.
|
|
19
|
+
*/
|
|
20
|
+
export function useImageFallback(...candidates: (string | null | undefined)[]): ImageFallback {
|
|
21
|
+
const [failed, setFailed] = useState<ReadonlySet<string>>(() => new Set());
|
|
22
|
+
|
|
23
|
+
const src = candidates.find((candidate): candidate is string => !!candidate && !failed.has(candidate)) ?? null;
|
|
24
|
+
|
|
25
|
+
const onError = useCallback(() => {
|
|
26
|
+
if (!src) return;
|
|
27
|
+
setFailed(prev => new Set(prev).add(src));
|
|
28
|
+
}, [src]);
|
|
29
|
+
|
|
30
|
+
return { src, onError };
|
|
31
|
+
}
|
package/src/utils/app-origins.ts
CHANGED
|
@@ -50,12 +50,17 @@ export function readAppOrigins(): AppOrigins {
|
|
|
50
50
|
/** Called when a host registers, replaces or clears its auth adapter, which can change `allowedOrigins`. */
|
|
51
51
|
export const subscribeAppOrigins: (listener: () => void) => () => void = subscribeRegisteredAdapter;
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*/
|
|
53
|
+
/** Scheme and host, not `URL.origin`, which is `"null"` for a `capacitor://` page. */
|
|
54
|
+
function isOnPage(target: URL, page: URL): boolean {
|
|
55
|
+
return target.protocol === page.protocol && target.host === page.host;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Whether `target` is on the page's own origin rather than on an origin the host sanctioned. */
|
|
59
|
+
export function isPageOriginUrl(target: URL, origins: AppOrigins = readAppOrigins()): boolean {
|
|
60
|
+
return isOnPage(target, new URL(origins.pageUrl ?? SERVER_PAGE_URL));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `url` resolved against the page when it belongs to the app, otherwise `null`. */
|
|
59
64
|
export function resolveAppOriginUrl(url: string, origins: AppOrigins = readAppOrigins()): URL | null {
|
|
60
65
|
let page: URL;
|
|
61
66
|
let target: URL;
|
|
@@ -65,7 +70,7 @@ export function resolveAppOriginUrl(url: string, origins: AppOrigins = readAppOr
|
|
|
65
70
|
} catch {
|
|
66
71
|
return null;
|
|
67
72
|
}
|
|
68
|
-
if (target
|
|
73
|
+
if (isOnPage(target, page)) return target;
|
|
69
74
|
if (target.protocol !== 'http:' && target.protocol !== 'https:') return null;
|
|
70
75
|
return origins.allowedOrigins.includes(target.origin) ? target : null;
|
|
71
76
|
}
|