@civitai/blocks-react 0.56.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -2
- package/dist/api/generationResources.d.ts +4 -3
- package/dist/hooks/returnTypeLedger.d.ts +2 -0
- package/dist/hooks/returnTypeLedger.js +2 -0
- package/dist/hooks/useAppStorage.js +2 -2
- package/dist/hooks/useAppWorkflows.d.ts +5 -2
- package/dist/hooks/useAppWorkflows.js +18 -7
- package/dist/hooks/useBlockAnalytics.d.ts +5 -3
- package/dist/hooks/useBlockAnalytics.js +1 -1
- package/dist/hooks/useBlockBreakpoint.d.ts +7 -1
- package/dist/hooks/useBlockContext.d.ts +13 -2
- package/dist/hooks/useBlockContext.js +1 -1
- package/dist/hooks/useBlockResize.d.ts +7 -1
- package/dist/hooks/useBlockResize.js +1 -1
- package/dist/hooks/useBlockSettings.d.ts +6 -1
- package/dist/hooks/useBlockTheme.d.ts +6 -1
- package/dist/hooks/useBlockToken.d.ts +8 -3
- package/dist/hooks/useBlockToken.js +2 -2
- package/dist/hooks/useBuzzAccounts.d.ts +3 -2
- package/dist/hooks/useBuzzAccounts.js +16 -15
- package/dist/hooks/useBuzzBalance.d.ts +3 -2
- package/dist/hooks/useBuzzBalance.js +16 -18
- package/dist/hooks/useBuzzPurchase.d.ts +8 -6
- package/dist/hooks/useBuzzPurchase.js +3 -3
- package/dist/hooks/useBuzzTransactions.d.ts +4 -2
- package/dist/hooks/useBuzzTransactions.js +16 -15
- package/dist/hooks/useBuzzWorkflow.d.ts +71 -28
- package/dist/hooks/useBuzzWorkflow.js +31 -17
- package/dist/hooks/useCheckpointPicker.d.ts +18 -16
- package/dist/hooks/useCheckpointPicker.js +3 -3
- package/dist/hooks/useCivitaiNavigate.d.ts +5 -3
- package/dist/hooks/useCivitaiNavigate.js +1 -1
- package/dist/hooks/useCollectionFollow.js +4 -4
- package/dist/hooks/useConsentUnavailable.js +2 -2
- package/dist/hooks/useCreatePostFromApp.js +4 -4
- package/dist/hooks/useDailyCompensation.d.ts +4 -2
- package/dist/hooks/useDailyCompensation.js +17 -15
- package/dist/hooks/useDirectLoad.d.ts +7 -1
- package/dist/hooks/useDirectLoad.js +1 -1
- package/dist/hooks/useDomainMaturity.d.ts +6 -2
- package/dist/hooks/useGatedImages.d.ts +8 -0
- package/dist/hooks/useGatedImages.js +10 -2
- package/dist/hooks/useGenerationResources.d.ts +5 -3
- package/dist/hooks/useHostOrigin.d.ts +7 -1
- package/dist/hooks/useHostOrigin.js +1 -1
- package/dist/hooks/useImageUpload.d.ts +44 -17
- package/dist/hooks/useImageUpload.js +24 -4
- package/dist/hooks/usePublishGenerationOutputs.js +4 -4
- package/dist/hooks/useRequestConsent.d.ts +7 -5
- package/dist/hooks/useRequestConsent.js +1 -1
- package/dist/hooks/useRequestSequencer.d.ts +71 -0
- package/dist/hooks/useRequestSequencer.js +28 -0
- package/dist/hooks/useRequestSignIn.d.ts +7 -5
- package/dist/hooks/useRequestSignIn.js +1 -1
- package/dist/hooks/useResourcePicker.d.ts +16 -14
- package/dist/hooks/useResourcePicker.js +3 -3
- package/dist/hooks/useSaveImage.js +2 -2
- package/dist/hooks/useSharedStorage.js +2 -2
- package/dist/hooks/useTip.js +1 -1
- package/dist/hooks/useTipAllowance.d.ts +13 -1
- package/dist/hooks/useTipAllowance.js +49 -11
- package/dist/hooks/useViewer.d.ts +3 -2
- package/dist/hooks/useViewer.js +16 -18
- package/dist/hooks/useWildcardPack.d.ts +3 -1
- package/dist/hooks/useWildcardPack.js +19 -14
- package/dist/index.d.ts +30 -14
- package/dist/index.js +7 -7
- package/dist/internal/consentRefusalLatch.d.ts +1 -1
- package/dist/internal/consentRefusalLatch.js +1 -1
- package/dist/internal/liveHost.d.ts +63 -11
- package/dist/internal/liveHost.js +240 -37
- package/dist/internal/mockHost.js +16 -15
- package/dist/internal/pickerOverlay.d.ts +52 -4
- package/dist/internal/pickerOverlay.js +14 -3
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{internal → transport}/iframeTransport.d.ts +43 -5
- package/dist/{internal → transport}/iframeTransport.js +142 -12
- package/dist/transport/originMatcher.d.ts +92 -0
- package/dist/transport/originMatcher.js +222 -0
- package/dist/transport/requestId.d.ts +113 -0
- package/dist/transport/requestId.js +117 -0
- package/dist/{internal → transport}/validate.d.ts +34 -0
- package/dist/{internal → transport}/validate.js +189 -48
- package/dist/ui/BlockGate.js +1 -1
- package/dist/ui/SettingsForm.d.ts +8 -2
- package/dist/ui/SettingsForm.js +49 -3
- package/package.json +7 -148
- package/dist/api/generationResources.d.ts.map +0 -1
- package/dist/api/generationResources.js.map +0 -1
- package/dist/hooks/SfwGate.d.ts.map +0 -1
- package/dist/hooks/SfwGate.js.map +0 -1
- package/dist/hooks/useAppStorage.d.ts.map +0 -1
- package/dist/hooks/useAppStorage.js.map +0 -1
- package/dist/hooks/useAppWorkflows.d.ts.map +0 -1
- package/dist/hooks/useAppWorkflows.js.map +0 -1
- package/dist/hooks/useBlockAnalytics.d.ts.map +0 -1
- package/dist/hooks/useBlockAnalytics.js.map +0 -1
- package/dist/hooks/useBlockBreakpoint.d.ts.map +0 -1
- package/dist/hooks/useBlockBreakpoint.js.map +0 -1
- package/dist/hooks/useBlockContext.d.ts.map +0 -1
- package/dist/hooks/useBlockContext.js.map +0 -1
- package/dist/hooks/useBlockResize.d.ts.map +0 -1
- package/dist/hooks/useBlockResize.js.map +0 -1
- package/dist/hooks/useBlockSettings.d.ts.map +0 -1
- package/dist/hooks/useBlockSettings.js.map +0 -1
- package/dist/hooks/useBlockTheme.d.ts.map +0 -1
- package/dist/hooks/useBlockTheme.js.map +0 -1
- package/dist/hooks/useBlockToken.d.ts.map +0 -1
- package/dist/hooks/useBlockToken.js.map +0 -1
- package/dist/hooks/useBuzzAccounts.d.ts.map +0 -1
- package/dist/hooks/useBuzzAccounts.js.map +0 -1
- package/dist/hooks/useBuzzBalance.d.ts.map +0 -1
- package/dist/hooks/useBuzzBalance.js.map +0 -1
- package/dist/hooks/useBuzzPurchase.d.ts.map +0 -1
- package/dist/hooks/useBuzzPurchase.js.map +0 -1
- package/dist/hooks/useBuzzTransactions.d.ts.map +0 -1
- package/dist/hooks/useBuzzTransactions.js.map +0 -1
- package/dist/hooks/useBuzzWorkflow.d.ts.map +0 -1
- package/dist/hooks/useBuzzWorkflow.js.map +0 -1
- package/dist/hooks/useCheckpointPicker.d.ts.map +0 -1
- package/dist/hooks/useCheckpointPicker.js.map +0 -1
- package/dist/hooks/useCivitaiNavigate.d.ts.map +0 -1
- package/dist/hooks/useCivitaiNavigate.js.map +0 -1
- package/dist/hooks/useCollectionFollow.d.ts.map +0 -1
- package/dist/hooks/useCollectionFollow.js.map +0 -1
- package/dist/hooks/useConsentUnavailable.d.ts.map +0 -1
- package/dist/hooks/useConsentUnavailable.js.map +0 -1
- package/dist/hooks/useCreatePostFromApp.d.ts.map +0 -1
- package/dist/hooks/useCreatePostFromApp.js.map +0 -1
- package/dist/hooks/useDailyCompensation.d.ts.map +0 -1
- package/dist/hooks/useDailyCompensation.js.map +0 -1
- package/dist/hooks/useDirectLoad.d.ts.map +0 -1
- package/dist/hooks/useDirectLoad.js.map +0 -1
- package/dist/hooks/useDomainMaturity.d.ts.map +0 -1
- package/dist/hooks/useDomainMaturity.js.map +0 -1
- package/dist/hooks/useGatedImages.d.ts.map +0 -1
- package/dist/hooks/useGatedImages.js.map +0 -1
- package/dist/hooks/useGenerationResources.d.ts.map +0 -1
- package/dist/hooks/useGenerationResources.js.map +0 -1
- package/dist/hooks/useHostOrigin.d.ts.map +0 -1
- package/dist/hooks/useHostOrigin.js.map +0 -1
- package/dist/hooks/useImageUpload.d.ts.map +0 -1
- package/dist/hooks/useImageUpload.js.map +0 -1
- package/dist/hooks/usePublishGenerationOutputs.d.ts.map +0 -1
- package/dist/hooks/usePublishGenerationOutputs.js.map +0 -1
- package/dist/hooks/useRequestConsent.d.ts.map +0 -1
- package/dist/hooks/useRequestConsent.js.map +0 -1
- package/dist/hooks/useRequestSignIn.d.ts.map +0 -1
- package/dist/hooks/useRequestSignIn.js.map +0 -1
- package/dist/hooks/useResourcePicker.d.ts.map +0 -1
- package/dist/hooks/useResourcePicker.js.map +0 -1
- package/dist/hooks/useSaveImage.d.ts.map +0 -1
- package/dist/hooks/useSaveImage.js.map +0 -1
- package/dist/hooks/useSharedStorage.d.ts.map +0 -1
- package/dist/hooks/useSharedStorage.js.map +0 -1
- package/dist/hooks/useTip.d.ts.map +0 -1
- package/dist/hooks/useTip.js.map +0 -1
- package/dist/hooks/useTipAllowance.d.ts.map +0 -1
- package/dist/hooks/useTipAllowance.js.map +0 -1
- package/dist/hooks/useViewer.d.ts.map +0 -1
- package/dist/hooks/useViewer.js.map +0 -1
- package/dist/hooks/useWildcardPack.d.ts.map +0 -1
- package/dist/hooks/useWildcardPack.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal/catalog.d.ts.map +0 -1
- package/dist/internal/catalog.js.map +0 -1
- package/dist/internal/consent.d.ts.map +0 -1
- package/dist/internal/consent.js.map +0 -1
- package/dist/internal/consentRefusalLatch.d.ts.map +0 -1
- package/dist/internal/consentRefusalLatch.js.map +0 -1
- package/dist/internal/detector.d.ts.map +0 -1
- package/dist/internal/detector.js.map +0 -1
- package/dist/internal/directLoad.d.ts.map +0 -1
- package/dist/internal/directLoad.js.map +0 -1
- package/dist/internal/iframeTransport.d.ts.map +0 -1
- package/dist/internal/iframeTransport.js.map +0 -1
- package/dist/internal/inlineTransport.d.ts.map +0 -1
- package/dist/internal/inlineTransport.js.map +0 -1
- package/dist/internal/liveHost.d.ts.map +0 -1
- package/dist/internal/liveHost.js.map +0 -1
- package/dist/internal/mockHost.d.ts.map +0 -1
- package/dist/internal/mockHost.js.map +0 -1
- package/dist/internal/originMatcher.d.ts +0 -28
- package/dist/internal/originMatcher.d.ts.map +0 -1
- package/dist/internal/originMatcher.js +0 -89
- package/dist/internal/originMatcher.js.map +0 -1
- package/dist/internal/pickerOverlay.d.ts.map +0 -1
- package/dist/internal/pickerOverlay.js.map +0 -1
- package/dist/internal/replyError.d.ts.map +0 -1
- package/dist/internal/replyError.js.map +0 -1
- package/dist/internal/requestTimeouts.d.ts.map +0 -1
- package/dist/internal/requestTimeouts.js.map +0 -1
- package/dist/internal/singleton.d.ts.map +0 -1
- package/dist/internal/singleton.js.map +0 -1
- package/dist/internal/transport.d.ts.map +0 -1
- package/dist/internal/transport.js.map +0 -1
- package/dist/internal/validate.d.ts.map +0 -1
- package/dist/internal/validate.js.map +0 -1
- package/dist/live.d.ts.map +0 -1
- package/dist/live.js.map +0 -1
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js.map +0 -1
- package/dist/ui/Alert.d.ts.map +0 -1
- package/dist/ui/Alert.js.map +0 -1
- package/dist/ui/Badge.d.ts.map +0 -1
- package/dist/ui/Badge.js.map +0 -1
- package/dist/ui/BlockGate.d.ts.map +0 -1
- package/dist/ui/BlockGate.js.map +0 -1
- package/dist/ui/Button.d.ts.map +0 -1
- package/dist/ui/Button.js.map +0 -1
- package/dist/ui/Card.d.ts.map +0 -1
- package/dist/ui/Card.js.map +0 -1
- package/dist/ui/Collapse.d.ts.map +0 -1
- package/dist/ui/Collapse.js.map +0 -1
- package/dist/ui/FollowButton.d.ts.map +0 -1
- package/dist/ui/FollowButton.js.map +0 -1
- package/dist/ui/Group.d.ts.map +0 -1
- package/dist/ui/Group.js.map +0 -1
- package/dist/ui/Loader.d.ts.map +0 -1
- package/dist/ui/Loader.js.map +0 -1
- package/dist/ui/Modal.d.ts.map +0 -1
- package/dist/ui/Modal.js.map +0 -1
- package/dist/ui/NumberInput.d.ts.map +0 -1
- package/dist/ui/NumberInput.js.map +0 -1
- package/dist/ui/ReportButton.d.ts.map +0 -1
- package/dist/ui/ReportButton.js.map +0 -1
- package/dist/ui/ResourceCard.d.ts.map +0 -1
- package/dist/ui/ResourceCard.js.map +0 -1
- package/dist/ui/SegmentedControl.d.ts.map +0 -1
- package/dist/ui/SegmentedControl.js.map +0 -1
- package/dist/ui/Select.d.ts.map +0 -1
- package/dist/ui/Select.js.map +0 -1
- package/dist/ui/SettingsForm.d.ts.map +0 -1
- package/dist/ui/SettingsForm.js.map +0 -1
- package/dist/ui/Slider.d.ts.map +0 -1
- package/dist/ui/Slider.js.map +0 -1
- package/dist/ui/Stack.d.ts.map +0 -1
- package/dist/ui/Stack.js.map +0 -1
- package/dist/ui/TextInput.d.ts.map +0 -1
- package/dist/ui/TextInput.js.map +0 -1
- package/dist/ui/Textarea.d.ts.map +0 -1
- package/dist/ui/Textarea.js.map +0 -1
- package/dist/ui/TipButton.d.ts.map +0 -1
- package/dist/ui/TipButton.js.map +0 -1
- package/dist/ui/index.d.ts.map +0 -1
- package/dist/ui/index.js.map +0 -1
- package/dist/ui/styles.d.ts.map +0 -1
- package/dist/ui/styles.js.map +0 -1
- /package/dist/{internal → transport}/detector.d.ts +0 -0
- /package/dist/{internal → transport}/detector.js +0 -0
- /package/dist/{internal → transport}/directLoad.d.ts +0 -0
- /package/dist/{internal → transport}/directLoad.js +0 -0
- /package/dist/{internal → transport}/inlineTransport.d.ts +0 -0
- /package/dist/{internal → transport}/inlineTransport.js +0 -0
- /package/dist/{internal → transport}/requestTimeouts.d.ts +0 -0
- /package/dist/{internal → transport}/requestTimeouts.js +0 -0
- /package/dist/{internal → transport}/singleton.d.ts +0 -0
- /package/dist/{internal → transport}/singleton.js +0 -0
- /package/dist/{internal → transport}/transport.d.ts +0 -0
- /package/dist/{internal → transport}/transport.js +0 -0
|
@@ -206,11 +206,22 @@ export function openPickerOverlay(opts) {
|
|
|
206
206
|
opts.onResolve(selection);
|
|
207
207
|
};
|
|
208
208
|
const selectCard = (card) => {
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
// 🔴 BRANCH ON THE REPLY CHANNEL, NOT ON `opts.type` (#391). The channel is
|
|
210
|
+
// what the block's consumers read the payload as; the requested type is
|
|
211
|
+
// only what the catalog was filtered by. Branching on the type sent a
|
|
212
|
+
// `cardToCheckpoint()` projection — no `modelType` — down
|
|
213
|
+
// `RESOURCE_PICKER_RESULT` for every `resourceType: 'Checkpoint'` request,
|
|
214
|
+
// and `BlockResourceInfo.modelType` is REQUIRED.
|
|
215
|
+
//
|
|
216
|
+
// `cardToResource` already resolves `modelType` correctly for both: it
|
|
217
|
+
// prefers the card's own REST-reported type and falls back to `opts.type`,
|
|
218
|
+
// so a Checkpoint asked for on the resource channel comes back
|
|
219
|
+
// `modelType: 'Checkpoint'`.
|
|
220
|
+
if (opts.resultChannel === 'CHECKPOINT_PICKER_RESULT') {
|
|
221
|
+
resolve({ channel: 'CHECKPOINT_PICKER_RESULT', selected: cardToCheckpoint(card) });
|
|
211
222
|
}
|
|
212
223
|
else {
|
|
213
|
-
resolve({
|
|
224
|
+
resolve({ channel: 'RESOURCE_PICKER_RESULT', selected: cardToResource(card, opts.type) });
|
|
214
225
|
}
|
|
215
226
|
};
|
|
216
227
|
const handle = {
|
package/dist/testing.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* README section, failing on growth and on shrinkage alike.
|
|
18
18
|
*/
|
|
19
19
|
import { type ReactNode } from 'react';
|
|
20
|
-
import { __resetTransport } from './
|
|
20
|
+
import { __resetTransport } from './transport/singleton.js';
|
|
21
21
|
import { type MockHostOptions } from './internal/mockHost.js';
|
|
22
22
|
export { __resetTransport as resetTransport };
|
|
23
23
|
export { createMockHost, readMockHostUrlOptions, type MockHost, type MockHostOptions, type MockHostFailMode, type MockHostScenarioPatch, type MockGenerationScenario, type MockBuzzScenario, type MockBuzzBalance, type MockBuzzHandle, type MockStorageScenario, type MockSharedScenario, type MockSharedSeed, type MockCannedImageScan, type CostSpec, type ImageSpec, type CannedPick, } from './internal/mockHost.js';
|
package/dist/testing.js
CHANGED
|
@@ -18,7 +18,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
18
18
|
* README section, failing on growth and on shrinkage alike.
|
|
19
19
|
*/
|
|
20
20
|
import { useEffect, useRef, useState } from 'react';
|
|
21
|
-
import { __resetTransport } from './
|
|
21
|
+
import { __resetTransport } from './transport/singleton.js';
|
|
22
22
|
import { createMockHost, readMockHostUrlOptions, } from './internal/mockHost.js';
|
|
23
23
|
export { __resetTransport as resetTransport };
|
|
24
24
|
export { createMockHost, readMockHostUrlOptions, } from './internal/mockHost.js';
|
|
@@ -4,7 +4,16 @@ export interface IframeTransportOptions {
|
|
|
4
4
|
/**
|
|
5
5
|
* Origins from which `BLOCK_INIT` (and any other inbound message) is
|
|
6
6
|
* accepted. MUST contain at least one entry. Messages from any other
|
|
7
|
-
* origin — including the local origin — are dropped
|
|
7
|
+
* origin — including the local origin — are dropped; a drop that happens
|
|
8
|
+
* before init warns ONCE per distinct origin and is named in the
|
|
9
|
+
* init-timeout error (see {@link MAX_TRACKED_ORIGINS}).
|
|
10
|
+
*
|
|
11
|
+
* Entries are canonicalised by `OriginMatcher`, so a trailing slash, a
|
|
12
|
+
* mixed-case scheme/host and an explicit DEFAULT port all match the
|
|
13
|
+
* equivalent `event.origin` — while a non-default port, the scheme and the
|
|
14
|
+
* exact host stay significant. An entry that is not a bare origin THROWS
|
|
15
|
+
* here rather than being silently skipped. Read that file's docblock before
|
|
16
|
+
* changing anything about the comparison: it is a security boundary.
|
|
8
17
|
*
|
|
9
18
|
* Typically wired from `import.meta.env.VITE_BLOCK_ALLOWED_PARENT_ORIGINS`
|
|
10
19
|
* (or the framework's equivalent) at block-app startup.
|
|
@@ -22,13 +31,34 @@ export interface IframeTransportOptions {
|
|
|
22
31
|
export declare class IframeTransport implements BlockTransport {
|
|
23
32
|
private readonly originMatcher;
|
|
24
33
|
/**
|
|
25
|
-
* The EXACT (non-wildcard) entries of `allowedParentOrigins`,
|
|
26
|
-
* `postMessage` `targetOrigin`. A wildcard
|
|
27
|
-
* is not a concrete origin and cannot be a
|
|
28
|
-
* see {@link announceReady} for what
|
|
34
|
+
* The EXACT (non-wildcard) entries of `allowedParentOrigins`, NORMALISED by
|
|
35
|
+
* `OriginMatcher` and usable as a `postMessage` `targetOrigin`. A wildcard
|
|
36
|
+
* entry (`https://*.civitaic.com`) is not a concrete origin and cannot be a
|
|
37
|
+
* target, so it is excluded there — see {@link announceReady} for what
|
|
38
|
+
* happens when nothing exact remains.
|
|
29
39
|
*/
|
|
30
40
|
private readonly exactAllowedOrigins;
|
|
41
|
+
/**
|
|
42
|
+
* The allowlist AS CONFIGURED (trimmed, not normalised). Named in the
|
|
43
|
+
* init-timeout error so the operator sees the strings they actually wrote
|
|
44
|
+
* next to the origins that actually arrived.
|
|
45
|
+
*/
|
|
46
|
+
private readonly configuredOrigins;
|
|
31
47
|
private readonly window;
|
|
48
|
+
/**
|
|
49
|
+
* Origins observed BEFORE init resolved, split by what the allowlist gate did
|
|
50
|
+
* with them. Both bounded — see {@link MAX_TRACKED_ORIGINS}.
|
|
51
|
+
*
|
|
52
|
+
* 🔴 THE TWO BUCKETS ARE DIFFERENT DIAGNOSES AND AN EMPTY PAIR IS A THIRD.
|
|
53
|
+
* "rejected" says the host is talking and the allowlist is wrong; "accepted,
|
|
54
|
+
* but nothing was a valid BLOCK_INIT" says the allowlist is right and the
|
|
55
|
+
* payload or the message type is wrong; neither means the host frame never
|
|
56
|
+
* posted at all. Collapsing them would send the one person reading this error
|
|
57
|
+
* to the wrong half of the system — the failure this whole diagnostic exists
|
|
58
|
+
* to prevent.
|
|
59
|
+
*/
|
|
60
|
+
private readonly acceptedOriginTally;
|
|
61
|
+
private readonly rejectedOriginTally;
|
|
32
62
|
private snapshot;
|
|
33
63
|
private readonly listeners;
|
|
34
64
|
/** Origin of the parent — captured from the first valid `BLOCK_INIT`. */
|
|
@@ -71,6 +101,14 @@ export declare class IframeTransport implements BlockTransport {
|
|
|
71
101
|
* chose to frame.
|
|
72
102
|
*/
|
|
73
103
|
private announceReady;
|
|
104
|
+
/**
|
|
105
|
+
* One sentence naming the origins this transport actually heard from before
|
|
106
|
+
* init timed out — the single fact that turns "BLOCK_INIT never arrived" from
|
|
107
|
+
* a guess into a diagnosis.
|
|
108
|
+
*
|
|
109
|
+
* Three outcomes, deliberately worded apart (see {@link acceptedOriginTally}).
|
|
110
|
+
*/
|
|
111
|
+
private describeObservedOrigins;
|
|
74
112
|
getSnapshot(): BlockSnapshot;
|
|
75
113
|
/**
|
|
76
114
|
* The validated parent origin — `null` until `BLOCK_INIT` lands.
|
|
@@ -2,8 +2,52 @@ import { boundBlockToParentMessageType, isMessage, OTHER_MESSAGE_TYPE_LABEL, par
|
|
|
2
2
|
import { EMPTY_SNAPSHOT, nextRequestId, RequestTimeoutError, snapshotFromInit, tokenFromWrapped, } from './transport.js';
|
|
3
3
|
import { OriginMatcher } from './originMatcher.js';
|
|
4
4
|
import { DEFAULT_REQUEST_TIMEOUT_MS } from './requestTimeouts.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isRoutableRequestId } from './requestId.js';
|
|
6
|
+
import { payloadValidatorFor, projectInboundPayload } from './validate.js';
|
|
6
7
|
const INIT_TIMEOUT_MS = 10_000;
|
|
8
|
+
/**
|
|
9
|
+
* How many DISTINCT inbound origins are remembered for the init-timeout
|
|
10
|
+
* diagnostic, per bucket (accepted / rejected).
|
|
11
|
+
*
|
|
12
|
+
* Bounded on purpose: a framing page — or any browser extension sharing the
|
|
13
|
+
* window — can postMessage from an unbounded number of origins, and the host
|
|
14
|
+
* itself re-sends `BLOCK_INIT` on a ~400ms tick, so an uncapped set would let a
|
|
15
|
+
* hostile or merely noisy page grow an error string without limit. Five is
|
|
16
|
+
* enough to name the misconfigured origin next to the configured allowlist,
|
|
17
|
+
* which is the whole diagnostic; past that the message says it is truncated
|
|
18
|
+
* rather than pretending the list is complete.
|
|
19
|
+
*/
|
|
20
|
+
const MAX_TRACKED_ORIGINS = 5;
|
|
21
|
+
function newTally() {
|
|
22
|
+
return { seen: new Set(), truncated: false };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Remembers `origin` if there is room. Returns true only the FIRST time an
|
|
26
|
+
* origin is recorded, so callers can attach a one-shot log to it without
|
|
27
|
+
* turning the host's 400ms init retry into a console flood.
|
|
28
|
+
*/
|
|
29
|
+
function recordOrigin(tally, origin) {
|
|
30
|
+
if (tally.seen.has(origin))
|
|
31
|
+
return false;
|
|
32
|
+
if (tally.seen.size >= MAX_TRACKED_ORIGINS) {
|
|
33
|
+
tally.truncated = true;
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
tally.seen.add(origin);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
/** The quoted, comma-separated list of origins remembered on one bucket. */
|
|
40
|
+
function formatOrigins(tally) {
|
|
41
|
+
return [...tally.seen].map((o) => `"${o}"`).join(', ');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Leading clause for the bucket's parenthetical when it overflowed, so a capped
|
|
45
|
+
* list is never read as a complete one. Empty when nothing was dropped — the
|
|
46
|
+
* common case, and the one where the list IS the whole truth.
|
|
47
|
+
*/
|
|
48
|
+
function truncationNote(tally) {
|
|
49
|
+
return tally.truncated ? `first ${MAX_TRACKED_ORIGINS} of more; ` : '';
|
|
50
|
+
}
|
|
7
51
|
/**
|
|
8
52
|
* Iframe-mode transport. Validates `event.origin` on every inbound message,
|
|
9
53
|
* awaits `BLOCK_INIT` with a 10s timeout (after which `waitForInit` rejects
|
|
@@ -13,13 +57,34 @@ const INIT_TIMEOUT_MS = 10_000;
|
|
|
13
57
|
export class IframeTransport {
|
|
14
58
|
originMatcher;
|
|
15
59
|
/**
|
|
16
|
-
* The EXACT (non-wildcard) entries of `allowedParentOrigins`,
|
|
17
|
-
* `postMessage` `targetOrigin`. A wildcard
|
|
18
|
-
* is not a concrete origin and cannot be a
|
|
19
|
-
* see {@link announceReady} for what
|
|
60
|
+
* The EXACT (non-wildcard) entries of `allowedParentOrigins`, NORMALISED by
|
|
61
|
+
* `OriginMatcher` and usable as a `postMessage` `targetOrigin`. A wildcard
|
|
62
|
+
* entry (`https://*.civitaic.com`) is not a concrete origin and cannot be a
|
|
63
|
+
* target, so it is excluded there — see {@link announceReady} for what
|
|
64
|
+
* happens when nothing exact remains.
|
|
20
65
|
*/
|
|
21
66
|
exactAllowedOrigins;
|
|
67
|
+
/**
|
|
68
|
+
* The allowlist AS CONFIGURED (trimmed, not normalised). Named in the
|
|
69
|
+
* init-timeout error so the operator sees the strings they actually wrote
|
|
70
|
+
* next to the origins that actually arrived.
|
|
71
|
+
*/
|
|
72
|
+
configuredOrigins;
|
|
22
73
|
window;
|
|
74
|
+
/**
|
|
75
|
+
* Origins observed BEFORE init resolved, split by what the allowlist gate did
|
|
76
|
+
* with them. Both bounded — see {@link MAX_TRACKED_ORIGINS}.
|
|
77
|
+
*
|
|
78
|
+
* 🔴 THE TWO BUCKETS ARE DIFFERENT DIAGNOSES AND AN EMPTY PAIR IS A THIRD.
|
|
79
|
+
* "rejected" says the host is talking and the allowlist is wrong; "accepted,
|
|
80
|
+
* but nothing was a valid BLOCK_INIT" says the allowlist is right and the
|
|
81
|
+
* payload or the message type is wrong; neither means the host frame never
|
|
82
|
+
* posted at all. Collapsing them would send the one person reading this error
|
|
83
|
+
* to the wrong half of the system — the failure this whole diagnostic exists
|
|
84
|
+
* to prevent.
|
|
85
|
+
*/
|
|
86
|
+
acceptedOriginTally = newTally();
|
|
87
|
+
rejectedOriginTally = newTally();
|
|
23
88
|
snapshot = EMPTY_SNAPSHOT;
|
|
24
89
|
listeners = new Set();
|
|
25
90
|
/** Origin of the parent — captured from the first valid `BLOCK_INIT`. */
|
|
@@ -48,9 +113,15 @@ export class IframeTransport {
|
|
|
48
113
|
// `https://*.example.com` entries match any subdomain on a dot boundary
|
|
49
114
|
// (mirrors the host-side CSP frame-ancestors convention).
|
|
50
115
|
this.originMatcher = new OriginMatcher(opts.allowedParentOrigins);
|
|
51
|
-
|
|
116
|
+
// From the matcher, NOT re-derived here: a second copy of "which entries are
|
|
117
|
+
// concrete origins, and how is one spelled" is a second place for the
|
|
118
|
+
// trailing-slash/case/default-port bug this class just fixed to come back —
|
|
119
|
+
// and it would come back specifically as a `postMessage` targetOrigin, where
|
|
120
|
+
// the failure is a silently dropped announce.
|
|
121
|
+
this.exactAllowedOrigins = this.originMatcher.exactOrigins;
|
|
122
|
+
this.configuredOrigins = opts.allowedParentOrigins
|
|
52
123
|
.map((entry) => entry.trim())
|
|
53
|
-
.filter((entry) => entry.length > 0
|
|
124
|
+
.filter((entry) => entry.length > 0);
|
|
54
125
|
this.window = opts.window ?? globalThis.window;
|
|
55
126
|
if (!this.window) {
|
|
56
127
|
throw new Error('IframeTransport: no window available; cannot mount on the server.');
|
|
@@ -63,6 +134,8 @@ export class IframeTransport {
|
|
|
63
134
|
if (!this.initResolved) {
|
|
64
135
|
this.initResolved = true;
|
|
65
136
|
this.rejectInit(new Error(`IframeTransport: timed out waiting for BLOCK_INIT after ${INIT_TIMEOUT_MS}ms. ` +
|
|
137
|
+
`${this.describeObservedOrigins()} ` +
|
|
138
|
+
`Configured allowedParentOrigins: ${this.configuredOrigins.map((o) => `"${o}"`).join(', ') || '(none)'}. ` +
|
|
66
139
|
'Verify the host frame is sending the init message and that its origin is in allowedParentOrigins.'));
|
|
67
140
|
}
|
|
68
141
|
}, INIT_TIMEOUT_MS);
|
|
@@ -171,6 +244,28 @@ export class IframeTransport {
|
|
|
171
244
|
}
|
|
172
245
|
}
|
|
173
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* One sentence naming the origins this transport actually heard from before
|
|
249
|
+
* init timed out — the single fact that turns "BLOCK_INIT never arrived" from
|
|
250
|
+
* a guess into a diagnosis.
|
|
251
|
+
*
|
|
252
|
+
* Three outcomes, deliberately worded apart (see {@link acceptedOriginTally}).
|
|
253
|
+
*/
|
|
254
|
+
describeObservedOrigins() {
|
|
255
|
+
const parts = [];
|
|
256
|
+
if (this.rejectedOriginTally.seen.size > 0) {
|
|
257
|
+
parts.push(`rejected messages from ${formatOrigins(this.rejectedOriginTally)} ` +
|
|
258
|
+
`(${truncationNote(this.rejectedOriginTally)}no allowedParentOrigins entry matched)`);
|
|
259
|
+
}
|
|
260
|
+
if (this.acceptedOriginTally.seen.size > 0) {
|
|
261
|
+
parts.push(`accepted messages from ${formatOrigins(this.acceptedOriginTally)} ` +
|
|
262
|
+
`(${truncationNote(this.acceptedOriginTally)}none of them was a valid BLOCK_INIT)`);
|
|
263
|
+
}
|
|
264
|
+
if (parts.length === 0) {
|
|
265
|
+
return 'No inbound message was received from any origin.';
|
|
266
|
+
}
|
|
267
|
+
return `Origins seen: ${parts.join('; ')}.`;
|
|
268
|
+
}
|
|
174
269
|
getSnapshot() {
|
|
175
270
|
return this.snapshot;
|
|
176
271
|
}
|
|
@@ -343,7 +438,7 @@ export class IframeTransport {
|
|
|
343
438
|
return { label: OTHER_MESSAGE_TYPE_LABEL, hung: 'pushed' };
|
|
344
439
|
}
|
|
345
440
|
const requestId = payload?.requestId;
|
|
346
|
-
if (
|
|
441
|
+
if (isRoutableRequestId(requestId)) {
|
|
347
442
|
const pending = this.pending.get(requestId);
|
|
348
443
|
// The same predicate `handleMessage` applies before it will SETTLE a reply.
|
|
349
444
|
// An id matching a request awaiting a DIFFERENT reply type names nothing we
|
|
@@ -432,8 +527,24 @@ export class IframeTransport {
|
|
|
432
527
|
this.window.parent.postMessage(msg, this.parentOrigin);
|
|
433
528
|
}
|
|
434
529
|
handleMessage(event) {
|
|
435
|
-
if (!this.originMatcher.matches(event.origin))
|
|
530
|
+
if (!this.originMatcher.matches(event.origin)) {
|
|
531
|
+
// Still a DROP — nothing below this line runs. What changed is that the
|
|
532
|
+
// drop is no longer invisible: an origin rejected before init lands is the
|
|
533
|
+
// single fact that diagnoses a misspelled allowlist entry, and it is
|
|
534
|
+
// otherwise unobservable from inside the iframe. Bounded to
|
|
535
|
+
// MAX_TRACKED_ORIGINS distinct origins, and `recordOrigin` returns true
|
|
536
|
+
// only on the first sighting, so the host's ~400ms init retry warns ONCE
|
|
537
|
+
// rather than 25 times per ready window.
|
|
538
|
+
if (!this.initResolved && recordOrigin(this.rejectedOriginTally, event.origin)) {
|
|
539
|
+
// eslint-disable-next-line no-console -- developer-facing diagnostic at a trust boundary
|
|
540
|
+
console.warn(`IframeTransport: dropping a message from "${event.origin}" — no ` +
|
|
541
|
+
'allowedParentOrigins entry matched. Configured: ' +
|
|
542
|
+
`${this.configuredOrigins.map((o) => `"${o}"`).join(', ') || '(none)'}.`);
|
|
543
|
+
}
|
|
436
544
|
return;
|
|
545
|
+
}
|
|
546
|
+
if (!this.initResolved)
|
|
547
|
+
recordOrigin(this.acceptedOriginTally, event.origin);
|
|
437
548
|
const data = event.data;
|
|
438
549
|
if (data == null || typeof data !== 'object' || typeof data.type !== 'string')
|
|
439
550
|
return;
|
|
@@ -513,10 +624,28 @@ export class IframeTransport {
|
|
|
513
624
|
return;
|
|
514
625
|
}
|
|
515
626
|
// For request/response replies, look up the pending entry by `requestId`.
|
|
516
|
-
|
|
627
|
+
//
|
|
628
|
+
// 🔴 PROJECTED, NOT RAW. This is the last point before an inbound payload
|
|
629
|
+
// crosses into block code — `pending.resolve` below and the push handlers
|
|
630
|
+
// further down are the only two deliveries, and BOTH read this binding.
|
|
631
|
+
// `projectInboundPayload` drops fields a consumer is not allowed to see
|
|
632
|
+
// (today: every key beyond `imageId`/`status` on a `hidden` gated image) and
|
|
633
|
+
// is identity for every other type. Validation said "deliver this message";
|
|
634
|
+
// this says "deliver these fields".
|
|
635
|
+
//
|
|
636
|
+
// The `TOKEN_REFRESH_RESPONSE` branch below still reads `data.payload` on
|
|
637
|
+
// purpose: it applies to the SNAPSHOT rather than handing anything to block
|
|
638
|
+
// code, and it needs the narrowing `isMessage` gave `data`. Every DELIVERY
|
|
639
|
+
// must read this binding instead — the raw object still carries whatever the
|
|
640
|
+
// host sent.
|
|
641
|
+
//
|
|
642
|
+
// It runs on the block's side of the boundary rather than in the hook
|
|
643
|
+
// because `getTransport` + `sendTypedRequest` are PUBLIC exports: a consumer
|
|
644
|
+
// that bypasses `useGatedImages()` still gets the projection.
|
|
645
|
+
const payload = projectInboundPayload(data.type, data.payload);
|
|
517
646
|
let pending;
|
|
518
647
|
let matchedRequestId = null;
|
|
519
|
-
if (payload &&
|
|
648
|
+
if (payload && isRoutableRequestId(payload.requestId)) {
|
|
520
649
|
const candidate = this.pending.get(payload.requestId);
|
|
521
650
|
if (candidate && candidate.responseType === data.type) {
|
|
522
651
|
pending = candidate;
|
|
@@ -549,8 +678,9 @@ export class IframeTransport {
|
|
|
549
678
|
// listeners, so they fall through to the no-op tail below unchanged.
|
|
550
679
|
const handlers = this.pushListeners.get(data.type);
|
|
551
680
|
if (handlers && handlers.size > 0) {
|
|
681
|
+
// `payload`, not `data.payload`: the projected view — see the binding above.
|
|
552
682
|
for (const handler of [...handlers])
|
|
553
|
-
handler(
|
|
683
|
+
handler(payload);
|
|
554
684
|
return;
|
|
555
685
|
}
|
|
556
686
|
if (isMessage(data, 'SUSPEND')) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Origin allowlist matching for {@link IframeTransport}.
|
|
3
|
+
*
|
|
4
|
+
* Each `allowedParentOrigins` entry is either:
|
|
5
|
+
* - an EXACT origin (`https://civitai.com`) — matched by string equality
|
|
6
|
+
* against the NORMALISED entry, or
|
|
7
|
+
* - a SUFFIX-WILDCARD origin (`https://*.civitaic.com`) — matches any
|
|
8
|
+
* `https://<sub>.civitaic.com`, where `<sub>` is one or more labels
|
|
9
|
+
* (single-label `pr-9` or a full subtree `a.b`), but NOT the bare apex
|
|
10
|
+
* `https://civitaic.com` and NOT a different registrable domain.
|
|
11
|
+
*
|
|
12
|
+
* The wildcard form mirrors the host-side CSP `frame-ancestors` convention
|
|
13
|
+
* (`https://*.civitaic.com`) so a single block build can trust both prod
|
|
14
|
+
* (`civitai.com`, an exact entry) and dynamic preview subdomains
|
|
15
|
+
* (`pr-N.civitaic.com`, a wildcard entry).
|
|
16
|
+
*
|
|
17
|
+
* ## Normalisation — ENTRIES ONLY, NEVER THE CANDIDATE
|
|
18
|
+
*
|
|
19
|
+
* 🔴 SECURITY DIRECTION. Entries are canonicalised at construction; the
|
|
20
|
+
* candidate handed to {@link OriginMatcher.matches} is compared RAW. That
|
|
21
|
+
* asymmetry is deliberate and must not be "tidied up": a real `event.origin`
|
|
22
|
+
* is already the serialization of a canonical origin (lowercase scheme + host,
|
|
23
|
+
* no trailing slash, default port elided, IDN in punycode), so normalising it
|
|
24
|
+
* again can only ADD accepts — e.g. `new URL('https://civitai.com/evil').origin`
|
|
25
|
+
* is `https://civitai.com`, so a `.origin` round-trip on the candidate would
|
|
26
|
+
* make a path-bearing string match an apex entry. Normalising only the entries
|
|
27
|
+
* is value-preserving on the set of REAL origins accepted while closing the
|
|
28
|
+
* spellings an operator actually writes.
|
|
29
|
+
*
|
|
30
|
+
* Equivalences deliberately ACCEPTED (an entry spelled this way matches the
|
|
31
|
+
* equivalent `event.origin`), all of them delegated to the URL parser so the
|
|
32
|
+
* rules are the WHATWG ones rather than hand-rolled string surgery:
|
|
33
|
+
* - trailing slash — `https://civitai.com/` ≡ `https://civitai.com`
|
|
34
|
+
* - scheme/host case — `HTTPS://CIVITAI.COM` ≡ `https://civitai.com`
|
|
35
|
+
* (RFC 3986 §3.1/§3.2.2; path/query case is NOT touched — an origin has
|
|
36
|
+
* neither, and an entry carrying one is rejected outright, below)
|
|
37
|
+
* - default port — `https://civitai.com:443` ≡ `https://civitai.com`,
|
|
38
|
+
* `http://x:80` ≡ `http://x`
|
|
39
|
+
* - IDN — `https://пример.com` ≡ `https://xn--e1afmkfd.com`, which is the
|
|
40
|
+
* form a browser puts on `event.origin`
|
|
41
|
+
*
|
|
42
|
+
* Distinctions deliberately KEPT (these are NOT equivalences — an entry and a
|
|
43
|
+
* candidate differing this way must not match):
|
|
44
|
+
* - a NON-default port stays significant: `https://civitai.com:8443` does not
|
|
45
|
+
* match `https://civitai.com`
|
|
46
|
+
* - the scheme stays significant: `http://` never matches `https://`
|
|
47
|
+
* - a trailing-dot (fully-qualified) host is a DIFFERENT origin to the browser,
|
|
48
|
+
* so `https://civitai.com.` and `https://civitai.com` stay distinct here too
|
|
49
|
+
* - no suffix/prefix logic on exact entries: `https://civitai.com.evil.com`
|
|
50
|
+
* does not match `https://civitai.com`
|
|
51
|
+
*
|
|
52
|
+
* Spellings deliberately REJECTED — loudly, by throwing from the constructor.
|
|
53
|
+
* A malformed entry that is silently dropped is this file's own bug one level
|
|
54
|
+
* up: the allowlist then misses an origin the operator believes is on it, and
|
|
55
|
+
* nothing says so. An entry must denote an ORIGIN and nothing more:
|
|
56
|
+
* - no scheme (`civitai.com`) — unparseable
|
|
57
|
+
* - userinfo (`https://user:pass@civitai.com`) — `.origin` would silently
|
|
58
|
+
* discard the credentials the author wrote
|
|
59
|
+
* - a path, query or fragment (`https://civitai.com/embed`) — `.origin` would
|
|
60
|
+
* silently discard them, widening the entry to the whole origin
|
|
61
|
+
* - a scheme with no origin of its own (`foo://bar`, `file:///x`), whose
|
|
62
|
+
* `.origin` serialises to the literal string `"null"` — the same string a
|
|
63
|
+
* sandboxed opaque origin puts on `event.origin`, so accepting it would
|
|
64
|
+
* allowlist every opaque frame at once
|
|
65
|
+
*
|
|
66
|
+
* Security: matching is scheme-pinned and suffix-anchored on a DOT boundary,
|
|
67
|
+
* so `https://*.civitaic.com` does NOT match `https://civitaic.com.attacker.tld`
|
|
68
|
+
* (different suffix) nor `https://evilcivitaic.com` (no dot boundary). A
|
|
69
|
+
* bare `*` or empty wildcard is rejected at construction.
|
|
70
|
+
*/
|
|
71
|
+
export declare class OriginMatcher {
|
|
72
|
+
private readonly exact;
|
|
73
|
+
private readonly wildcards;
|
|
74
|
+
/**
|
|
75
|
+
* The normalised EXACT origins, in first-seen order and de-duplicated.
|
|
76
|
+
*
|
|
77
|
+
* Exposed because `IframeTransport.announceReady` needs concrete
|
|
78
|
+
* `postMessage` targets and must derive them from the SAME normalisation as
|
|
79
|
+
* matching — one rule, one place. A wildcard is not a concrete origin and is
|
|
80
|
+
* absent here by construction.
|
|
81
|
+
*/
|
|
82
|
+
readonly exactOrigins: readonly string[];
|
|
83
|
+
constructor(allowedParentOrigins: readonly string[]);
|
|
84
|
+
/**
|
|
85
|
+
* True when `origin` is allowed by an exact or wildcard allowlist entry.
|
|
86
|
+
*
|
|
87
|
+
* 🔴 `origin` is compared AS GIVEN — see the normalisation note in the module
|
|
88
|
+
* docblock for why it is never round-tripped through `new URL()` here.
|
|
89
|
+
*/
|
|
90
|
+
matches(origin: string): boolean;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=originMatcher.d.ts.map
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Origin allowlist matching for {@link IframeTransport}.
|
|
3
|
+
*
|
|
4
|
+
* Each `allowedParentOrigins` entry is either:
|
|
5
|
+
* - an EXACT origin (`https://civitai.com`) — matched by string equality
|
|
6
|
+
* against the NORMALISED entry, or
|
|
7
|
+
* - a SUFFIX-WILDCARD origin (`https://*.civitaic.com`) — matches any
|
|
8
|
+
* `https://<sub>.civitaic.com`, where `<sub>` is one or more labels
|
|
9
|
+
* (single-label `pr-9` or a full subtree `a.b`), but NOT the bare apex
|
|
10
|
+
* `https://civitaic.com` and NOT a different registrable domain.
|
|
11
|
+
*
|
|
12
|
+
* The wildcard form mirrors the host-side CSP `frame-ancestors` convention
|
|
13
|
+
* (`https://*.civitaic.com`) so a single block build can trust both prod
|
|
14
|
+
* (`civitai.com`, an exact entry) and dynamic preview subdomains
|
|
15
|
+
* (`pr-N.civitaic.com`, a wildcard entry).
|
|
16
|
+
*
|
|
17
|
+
* ## Normalisation — ENTRIES ONLY, NEVER THE CANDIDATE
|
|
18
|
+
*
|
|
19
|
+
* 🔴 SECURITY DIRECTION. Entries are canonicalised at construction; the
|
|
20
|
+
* candidate handed to {@link OriginMatcher.matches} is compared RAW. That
|
|
21
|
+
* asymmetry is deliberate and must not be "tidied up": a real `event.origin`
|
|
22
|
+
* is already the serialization of a canonical origin (lowercase scheme + host,
|
|
23
|
+
* no trailing slash, default port elided, IDN in punycode), so normalising it
|
|
24
|
+
* again can only ADD accepts — e.g. `new URL('https://civitai.com/evil').origin`
|
|
25
|
+
* is `https://civitai.com`, so a `.origin` round-trip on the candidate would
|
|
26
|
+
* make a path-bearing string match an apex entry. Normalising only the entries
|
|
27
|
+
* is value-preserving on the set of REAL origins accepted while closing the
|
|
28
|
+
* spellings an operator actually writes.
|
|
29
|
+
*
|
|
30
|
+
* Equivalences deliberately ACCEPTED (an entry spelled this way matches the
|
|
31
|
+
* equivalent `event.origin`), all of them delegated to the URL parser so the
|
|
32
|
+
* rules are the WHATWG ones rather than hand-rolled string surgery:
|
|
33
|
+
* - trailing slash — `https://civitai.com/` ≡ `https://civitai.com`
|
|
34
|
+
* - scheme/host case — `HTTPS://CIVITAI.COM` ≡ `https://civitai.com`
|
|
35
|
+
* (RFC 3986 §3.1/§3.2.2; path/query case is NOT touched — an origin has
|
|
36
|
+
* neither, and an entry carrying one is rejected outright, below)
|
|
37
|
+
* - default port — `https://civitai.com:443` ≡ `https://civitai.com`,
|
|
38
|
+
* `http://x:80` ≡ `http://x`
|
|
39
|
+
* - IDN — `https://пример.com` ≡ `https://xn--e1afmkfd.com`, which is the
|
|
40
|
+
* form a browser puts on `event.origin`
|
|
41
|
+
*
|
|
42
|
+
* Distinctions deliberately KEPT (these are NOT equivalences — an entry and a
|
|
43
|
+
* candidate differing this way must not match):
|
|
44
|
+
* - a NON-default port stays significant: `https://civitai.com:8443` does not
|
|
45
|
+
* match `https://civitai.com`
|
|
46
|
+
* - the scheme stays significant: `http://` never matches `https://`
|
|
47
|
+
* - a trailing-dot (fully-qualified) host is a DIFFERENT origin to the browser,
|
|
48
|
+
* so `https://civitai.com.` and `https://civitai.com` stay distinct here too
|
|
49
|
+
* - no suffix/prefix logic on exact entries: `https://civitai.com.evil.com`
|
|
50
|
+
* does not match `https://civitai.com`
|
|
51
|
+
*
|
|
52
|
+
* Spellings deliberately REJECTED — loudly, by throwing from the constructor.
|
|
53
|
+
* A malformed entry that is silently dropped is this file's own bug one level
|
|
54
|
+
* up: the allowlist then misses an origin the operator believes is on it, and
|
|
55
|
+
* nothing says so. An entry must denote an ORIGIN and nothing more:
|
|
56
|
+
* - no scheme (`civitai.com`) — unparseable
|
|
57
|
+
* - userinfo (`https://user:pass@civitai.com`) — `.origin` would silently
|
|
58
|
+
* discard the credentials the author wrote
|
|
59
|
+
* - a path, query or fragment (`https://civitai.com/embed`) — `.origin` would
|
|
60
|
+
* silently discard them, widening the entry to the whole origin
|
|
61
|
+
* - a scheme with no origin of its own (`foo://bar`, `file:///x`), whose
|
|
62
|
+
* `.origin` serialises to the literal string `"null"` — the same string a
|
|
63
|
+
* sandboxed opaque origin puts on `event.origin`, so accepting it would
|
|
64
|
+
* allowlist every opaque frame at once
|
|
65
|
+
*
|
|
66
|
+
* Security: matching is scheme-pinned and suffix-anchored on a DOT boundary,
|
|
67
|
+
* so `https://*.civitaic.com` does NOT match `https://civitaic.com.attacker.tld`
|
|
68
|
+
* (different suffix) nor `https://evilcivitaic.com` (no dot boundary). A
|
|
69
|
+
* bare `*` or empty wildcard is rejected at construction.
|
|
70
|
+
*/
|
|
71
|
+
/**
|
|
72
|
+
* Stand-in label substituted for `*` so a wildcard entry can be canonicalised
|
|
73
|
+
* by the SAME URL parse as an exact entry (see {@link parseWildcard}). It must
|
|
74
|
+
* be a valid DNS label that the URL parser leaves untouched, and distinctive
|
|
75
|
+
* enough that a real suffix cannot begin with it.
|
|
76
|
+
*/
|
|
77
|
+
const WILDCARD_PROBE_LABEL = 'civitai-wildcard-probe';
|
|
78
|
+
export class OriginMatcher {
|
|
79
|
+
exact;
|
|
80
|
+
wildcards;
|
|
81
|
+
/**
|
|
82
|
+
* The normalised EXACT origins, in first-seen order and de-duplicated.
|
|
83
|
+
*
|
|
84
|
+
* Exposed because `IframeTransport.announceReady` needs concrete
|
|
85
|
+
* `postMessage` targets and must derive them from the SAME normalisation as
|
|
86
|
+
* matching — one rule, one place. A wildcard is not a concrete origin and is
|
|
87
|
+
* absent here by construction.
|
|
88
|
+
*/
|
|
89
|
+
exactOrigins;
|
|
90
|
+
constructor(allowedParentOrigins) {
|
|
91
|
+
const exact = new Set();
|
|
92
|
+
const wildcards = [];
|
|
93
|
+
for (const raw of allowedParentOrigins) {
|
|
94
|
+
const entry = raw.trim();
|
|
95
|
+
if (!entry)
|
|
96
|
+
continue;
|
|
97
|
+
const wildcard = parseWildcard(entry);
|
|
98
|
+
if (wildcard) {
|
|
99
|
+
wildcards.push(wildcard);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
exact.add(normaliseOrigin(entry, entry));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
this.exact = exact;
|
|
106
|
+
this.exactOrigins = [...exact];
|
|
107
|
+
this.wildcards = wildcards;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* True when `origin` is allowed by an exact or wildcard allowlist entry.
|
|
111
|
+
*
|
|
112
|
+
* 🔴 `origin` is compared AS GIVEN — see the normalisation note in the module
|
|
113
|
+
* docblock for why it is never round-tripped through `new URL()` here.
|
|
114
|
+
*/
|
|
115
|
+
matches(origin) {
|
|
116
|
+
if (this.exact.has(origin))
|
|
117
|
+
return true;
|
|
118
|
+
for (const wc of this.wildcards) {
|
|
119
|
+
if (!origin.startsWith(wc.scheme))
|
|
120
|
+
continue;
|
|
121
|
+
const host = origin.slice(wc.scheme.length);
|
|
122
|
+
// Reject anything with a path/query smuggled into the host span:
|
|
123
|
+
// a real `event.origin` is scheme + host (+ optional :port). We require
|
|
124
|
+
// an exact host-suffix match with at least one leading label, and no '/'.
|
|
125
|
+
if (host.includes('/'))
|
|
126
|
+
continue;
|
|
127
|
+
// The host must END with the dot-anchored suffix AND have at least one
|
|
128
|
+
// character of label before the leading dot (so the apex itself is excluded).
|
|
129
|
+
if (host.length > wc.suffix.length && host.endsWith(wc.suffix)) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Canonicalises an allowlist entry to an origin serialization.
|
|
138
|
+
*
|
|
139
|
+
* `candidate` is what gets parsed; `entry` is what the operator wrote and is
|
|
140
|
+
* the only thing named in an error (for a wildcard the two differ — the probe
|
|
141
|
+
* label stands in for `*`). Throws on anything that is not, exactly, an origin.
|
|
142
|
+
*/
|
|
143
|
+
function normaliseOrigin(candidate, entry) {
|
|
144
|
+
let url;
|
|
145
|
+
try {
|
|
146
|
+
url = new URL(candidate);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
throw new Error(`IframeTransport: invalid allowed parent origin "${entry}". ` +
|
|
150
|
+
'Entries must be absolute origins including a scheme, e.g. "https://civitai.com".');
|
|
151
|
+
}
|
|
152
|
+
const extras = [];
|
|
153
|
+
if (url.username || url.password)
|
|
154
|
+
extras.push('credentials');
|
|
155
|
+
// An absolute URL with a special scheme always has a pathname of at least
|
|
156
|
+
// "/", so only a LONGER path is a real path. `""` covers non-special schemes,
|
|
157
|
+
// which are rejected by the origin check below anyway.
|
|
158
|
+
if (url.pathname !== '' && url.pathname !== '/')
|
|
159
|
+
extras.push('a path');
|
|
160
|
+
if (url.search)
|
|
161
|
+
extras.push('a query string');
|
|
162
|
+
if (url.hash)
|
|
163
|
+
extras.push('a fragment');
|
|
164
|
+
if (extras.length > 0) {
|
|
165
|
+
throw new Error(`IframeTransport: invalid allowed parent origin "${entry}" — it carries ${extras.join(', ')}. An allowlist entry must be a bare origin (scheme + host + optional port), ` +
|
|
166
|
+
'e.g. "https://civitai.com"; anything beyond that is not part of an origin and ' +
|
|
167
|
+
'would be silently discarded.');
|
|
168
|
+
}
|
|
169
|
+
if (url.origin === 'null') {
|
|
170
|
+
throw new Error(`IframeTransport: invalid allowed parent origin "${entry}" — the "${url.protocol}" ` +
|
|
171
|
+
'scheme has no origin of its own, so it serialises to the literal "null" that a ' +
|
|
172
|
+
'sandboxed opaque frame also reports. Use an http(s) origin, e.g. "https://civitai.com".');
|
|
173
|
+
}
|
|
174
|
+
return url.origin;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Parses a `https://*.example.com`-style entry into `{scheme, suffix}`.
|
|
178
|
+
* Returns `null` for non-wildcard (exact) entries.
|
|
179
|
+
* Throws for malformed wildcards (`*` only, `https://*`, `https://*.`).
|
|
180
|
+
*
|
|
181
|
+
* Canonicalisation reuses {@link normaliseOrigin} by substituting a concrete
|
|
182
|
+
* placeholder label for `*` and stripping it back off the parsed origin. That
|
|
183
|
+
* is what makes a wildcard entry obey exactly the same case, default-port,
|
|
184
|
+
* trailing-slash and rejection rules as an exact one instead of a second,
|
|
185
|
+
* drifting copy of them — `https://*.CIVITAIC.COM:443/` and
|
|
186
|
+
* `https://*.civitaic.com` describe the same set, and a wildcard carrying a
|
|
187
|
+
* path is rejected rather than turning into an entry that can never match
|
|
188
|
+
* (`matches` refuses any host span containing `/`).
|
|
189
|
+
*/
|
|
190
|
+
function parseWildcard(entry) {
|
|
191
|
+
const star = entry.indexOf('*');
|
|
192
|
+
if (star === -1)
|
|
193
|
+
return null;
|
|
194
|
+
// Wildcard must be of the exact form `<scheme>://*.<suffix>`.
|
|
195
|
+
const marker = '://*.';
|
|
196
|
+
const markerAt = entry.indexOf(marker);
|
|
197
|
+
if (markerAt === -1) {
|
|
198
|
+
throw new Error(`IframeTransport: invalid wildcard origin "${entry}". ` +
|
|
199
|
+
'Wildcard entries must look like "https://*.example.com".');
|
|
200
|
+
}
|
|
201
|
+
const scheme = entry.slice(0, markerAt + 3); // include "://"
|
|
202
|
+
const bareSuffix = entry.slice(markerAt + marker.length); // after "://*."
|
|
203
|
+
if (!scheme || scheme === '://' || !bareSuffix || bareSuffix.includes('*')) {
|
|
204
|
+
throw new Error(`IframeTransport: invalid wildcard origin "${entry}". ` +
|
|
205
|
+
'A wildcard needs a scheme and a non-empty domain suffix with exactly one "*", ' +
|
|
206
|
+
'e.g. "https://*.example.com".');
|
|
207
|
+
}
|
|
208
|
+
const origin = normaliseOrigin(`${scheme}${WILDCARD_PROBE_LABEL}.${bareSuffix}`, entry);
|
|
209
|
+
const sep = origin.indexOf('://');
|
|
210
|
+
const normalisedScheme = origin.slice(0, sep + 3);
|
|
211
|
+
const host = origin.slice(sep + 3);
|
|
212
|
+
const probePrefix = `${WILDCARD_PROBE_LABEL}.`;
|
|
213
|
+
const normalisedSuffix = host.startsWith(probePrefix) ? host.slice(probePrefix.length) : '';
|
|
214
|
+
if (!normalisedSuffix) {
|
|
215
|
+
throw new Error(`IframeTransport: invalid wildcard origin "${entry}". ` +
|
|
216
|
+
'A wildcard needs a scheme and a non-empty domain suffix with exactly one "*", ' +
|
|
217
|
+
'e.g. "https://*.example.com".');
|
|
218
|
+
}
|
|
219
|
+
// Dot-anchor the suffix so `*.civitaic.com` only matches on a label boundary.
|
|
220
|
+
return { scheme: normalisedScheme, suffix: `.${normalisedSuffix}` };
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=originMatcher.js.map
|