@civitai/blocks-react 0.63.2 → 0.64.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 +67 -6
- package/dist/hooks/useCreatePostFromApp.d.ts +7 -2
- package/dist/hooks/useCreatePostFromApp.js +8 -3
- package/dist/hooks/useNestedDocument.d.ts +73 -0
- package/dist/hooks/useNestedDocument.js +160 -0
- package/dist/hooks/usePublishGenerationOutputs.d.ts +6 -0
- package/dist/hooks/usePublishGenerationOutputs.js +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/internal/liveHost.d.ts +7 -2
- package/dist/internal/liveHost.js +36 -23
- package/dist/internal/mockHost.d.ts +33 -1
- package/dist/internal/mockHost.js +34 -0
- package/dist/transport/validate.js +86 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -111,6 +111,11 @@ writing: `useGatedImages()`, `usePublishGenerationOutputs()`, `useSaveImage()`,
|
|
|
111
111
|
[`TipButton`](#the-ui-subexport) row documents the intended usage and the
|
|
112
112
|
allowance-sharing rule. `useDirectLoad()` is covered under
|
|
113
113
|
[Direct-load fallback](#direct-load-fallback-open-on-civitai) rather than here.
|
|
114
|
+
**`dev:live` refuses `usePublishGenerationOutputs()`**: publishing requires the
|
|
115
|
+
viewer's signed-in civitai.com session, which the local harness does not have, so
|
|
116
|
+
`publish()` rejects with a message saying so. Test publishing against the mock
|
|
117
|
+
host — `createMockHost` or `Harness` from `@civitai/blocks-react/testing`, with
|
|
118
|
+
the `publishImageIds` / `publishError` options.
|
|
114
119
|
|
|
115
120
|
### `useBlockContext()`
|
|
116
121
|
|
|
@@ -724,8 +729,9 @@ async function share() {
|
|
|
724
729
|
}
|
|
725
730
|
```
|
|
726
731
|
|
|
727
|
-
|
|
728
|
-
|
|
732
|
+
With the mock host — `createMockHost` or `Harness` from
|
|
733
|
+
`@civitai/blocks-react/testing` — the `createPostResult` / `createPostError`
|
|
734
|
+
options drive both arms (including `declined`). **`dev:live` refuses this bridge on purpose** —
|
|
729
735
|
it has no civitai chrome to render the server-resolved confirm in, and driving
|
|
730
736
|
the write without it would let dev prove out a flow production does not have.
|
|
731
737
|
|
|
@@ -882,10 +888,9 @@ ceiling that lands on `null` like everything else. Two earlier drafts of this
|
|
|
882
888
|
section tried instead to enumerate the non-ceiling strings, and **both lists
|
|
883
889
|
were short**; see the header of `blocks/appStorageErrors.ts` for what they
|
|
884
890
|
missed and why no third list replaced them. `invalid block token` (an expired
|
|
885
|
-
token mid-session)
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
before writing copy for it.
|
|
891
|
+
token mid-session) and `block instance revoked` are *illustrations* of what
|
|
892
|
+
lands on `null`, never a bound on it. The practical consequence: `null` is a
|
|
893
|
+
busy bucket, so see the `default` arm note below before writing copy for it.
|
|
889
894
|
|
|
890
895
|
⚠️ **The mock reaches four of the six.** It models no app-wide umbrella
|
|
891
896
|
([#368](https://github.com/civitai/civitai-app-starters/issues/368)), so
|
|
@@ -1506,6 +1511,62 @@ const directLoad = useDirectLoad(); // true iff top-level AND no BLOC
|
|
|
1506
1511
|
const runUrl = hostToRunUrl('my-app.civit.ai'); // 'https://civitai.com/apps/run/my-app' | null (null = not a civit.ai host)
|
|
1507
1512
|
```
|
|
1508
1513
|
|
|
1514
|
+
## Embedding your own nested document
|
|
1515
|
+
|
|
1516
|
+
🔴 **A nested `<iframe src="…">` pointing at your own bundled content cannot
|
|
1517
|
+
load, and no manifest change fixes it** — your block document's origin is opaque
|
|
1518
|
+
and the static host stamps `frame-ancestors` + `X-Frame-Options` on every path
|
|
1519
|
+
it serves. The full derivation, the dated external browser matrix (an
|
|
1520
|
+
observation of the platform — nothing in this repo verifies it) and the known
|
|
1521
|
+
limits
|
|
1522
|
+
live in one place: the header of `@civitai/app-sdk`'s
|
|
1523
|
+
`src/blocks/nestedDocument.ts`.
|
|
1524
|
+
|
|
1525
|
+
**Prefer a single-document design.** An engine build (Defold, Unity WebGL,
|
|
1526
|
+
Phaser) is a `<canvas>` plus a JS loader that normally mounts straight into the
|
|
1527
|
+
document you hand it; point it at your block's own root element and the problem
|
|
1528
|
+
class disappears.
|
|
1529
|
+
|
|
1530
|
+
When a separate document is genuinely required, fetch it and inline it as the
|
|
1531
|
+
iframe's `srcdoc` — a `srcdoc` frame inherits your opaque origin and carries no
|
|
1532
|
+
response headers, so neither policy applies:
|
|
1533
|
+
|
|
1534
|
+
```tsx
|
|
1535
|
+
import { useNestedDocument } from '@civitai/blocks-react';
|
|
1536
|
+
|
|
1537
|
+
function Game() {
|
|
1538
|
+
const { srcDoc, status, error } = useNestedDocument({ src: '/game/index.html' });
|
|
1539
|
+
if (status === 'error') return <p>Could not load: {error?.message}</p>;
|
|
1540
|
+
if (status !== 'ready') return <p>Loading…</p>;
|
|
1541
|
+
// Your own sandbox — the nested frame does not inherit the outer one's.
|
|
1542
|
+
return <iframe title="game" sandbox="allow-scripts" srcDoc={srcDoc ?? undefined} />;
|
|
1543
|
+
}
|
|
1544
|
+
```
|
|
1545
|
+
|
|
1546
|
+
`status` is `'idle'` (no `src`), `'loading'`, `'ready'` or `'error'`; `srcDoc` is
|
|
1547
|
+
non-null exactly when `ready`, `error` exactly when `error`. Changing `src`
|
|
1548
|
+
restarts the fetch and aborts the previous one — `status` returns to `'loading'`
|
|
1549
|
+
in the same render that changes `src`, so no frame ever shows the previous
|
|
1550
|
+
document as `'ready'` beside a new `src`. Unmounting aborts it and writes no
|
|
1551
|
+
state. The fetch has a **30-second deadline**: a response that never arrives
|
|
1552
|
+
becomes `status: 'error'` with a message naming the timeout, rather than
|
|
1553
|
+
`'loading'` forever.
|
|
1554
|
+
|
|
1555
|
+
The helper injects an absolute `<base href="…/game/">` — the directory of the
|
|
1556
|
+
**response's final URL**, so a redirect is followed rather than resolved one
|
|
1557
|
+
directory too high — because a `srcdoc` document's base URL is `about:srcdoc`,
|
|
1558
|
+
inheriting yours, so relative asset loads would otherwise resolve to nothing. A
|
|
1559
|
+
document that already declares a `<base href>` has that one resolved against the
|
|
1560
|
+
final URL and replaced, so the base the parser uses is always absolute. It is
|
|
1561
|
+
**not** a de-duplication: markup declaring two `<base href>` tags comes back
|
|
1562
|
+
with two, the first (the only one a parser honours) rewritten and the second
|
|
1563
|
+
left inert. Only real markup counts — a `<base>` or a `<head>` inside a comment
|
|
1564
|
+
or an inline `<script>`/`<style>` body is ignored.
|
|
1565
|
+
|
|
1566
|
+
**The string is not sanitized**, on purpose: it is your own markup and scripts,
|
|
1567
|
+
verbatim. Only pass a `src` you control. Outside React, `fetchNestedDocument`
|
|
1568
|
+
from `@civitai/app-sdk/blocks` is the same thing without the hook.
|
|
1569
|
+
|
|
1509
1570
|
## The `/ui` subexport
|
|
1510
1571
|
|
|
1511
1572
|
Opinionated components, imported separately so a transport-only block stays lean.
|
|
@@ -58,8 +58,13 @@ export declare class CreatePostError extends Error {
|
|
|
58
58
|
*/
|
|
59
59
|
readonly declined: boolean;
|
|
60
60
|
/**
|
|
61
|
-
* There is no session
|
|
62
|
-
*
|
|
61
|
+
* There is no session — the viewer is signed out (`sign in to post`), or
|
|
62
|
+
* their session ended, e.g. signed out in another tab (a bare
|
|
63
|
+
* `UNAUTHORIZED`). Route this into `useRequestSignIn()` rather than showing
|
|
64
|
+
* an error — the viewer's next step is signing in, not retrying.
|
|
65
|
+
*
|
|
66
|
+
* A session that now belongs to a different account is NOT this: `.message`
|
|
67
|
+
* asks the viewer to reload the page, and is safe to render.
|
|
63
68
|
*/
|
|
64
69
|
readonly signInRequired: boolean;
|
|
65
70
|
constructor(error: string, opts?: {
|
|
@@ -79,8 +79,13 @@ export class CreatePostError extends Error {
|
|
|
79
79
|
*/
|
|
80
80
|
declined;
|
|
81
81
|
/**
|
|
82
|
-
* There is no session
|
|
83
|
-
*
|
|
82
|
+
* There is no session — the viewer is signed out (`sign in to post`), or
|
|
83
|
+
* their session ended, e.g. signed out in another tab (a bare
|
|
84
|
+
* `UNAUTHORIZED`). Route this into `useRequestSignIn()` rather than showing
|
|
85
|
+
* an error — the viewer's next step is signing in, not retrying.
|
|
86
|
+
*
|
|
87
|
+
* A session that now belongs to a different account is NOT this: `.message`
|
|
88
|
+
* asks the viewer to reload the page, and is safe to render.
|
|
84
89
|
*/
|
|
85
90
|
signInRequired;
|
|
86
91
|
constructor(error, opts) {
|
|
@@ -90,7 +95,7 @@ export class CreatePostError extends Error {
|
|
|
90
95
|
if (isCreatePostErrorCode(error))
|
|
91
96
|
this.code = error;
|
|
92
97
|
this.declined = error === 'declined';
|
|
93
|
-
this.signInRequired = error === 'sign in to post';
|
|
98
|
+
this.signInRequired = error === 'sign in to post' || error === 'UNAUTHORIZED';
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
/**
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the fetch has got to. `idle` is the no-`src` state, not a
|
|
3
|
+
* pre-fetch tick: a mount WITH a `src` goes straight to `loading`.
|
|
4
|
+
*/
|
|
5
|
+
export type NestedDocumentStatus = 'idle' | 'loading' | 'ready' | 'error';
|
|
6
|
+
/**
|
|
7
|
+
* What {@link useNestedDocument} returns. The three fields are driven from ONE
|
|
8
|
+
* state object, so they can never disagree: `srcDoc` is non-null exactly when
|
|
9
|
+
* `status === 'ready'`, and `error` is non-null exactly when
|
|
10
|
+
* `status === 'error'`.
|
|
11
|
+
*/
|
|
12
|
+
export interface UseNestedDocument {
|
|
13
|
+
/** The string to hand an iframe's `srcdoc`. Non-null iff `status === 'ready'`. */
|
|
14
|
+
srcDoc: string | null;
|
|
15
|
+
status: NestedDocumentStatus;
|
|
16
|
+
/** Non-null iff `status === 'error'`. A `NestedDocumentError` carries a `.code`. */
|
|
17
|
+
error: Error | null;
|
|
18
|
+
}
|
|
19
|
+
/** Options for {@link useNestedDocument}. */
|
|
20
|
+
export interface UseNestedDocumentOptions {
|
|
21
|
+
/**
|
|
22
|
+
* The nested document to inline. Relative URLs resolve against `baseUrl`, or
|
|
23
|
+
* the current document. `undefined` or `''` keeps the hook `idle` and fetches
|
|
24
|
+
* nothing — the shape to use while you are still deciding what to embed.
|
|
25
|
+
*/
|
|
26
|
+
src?: string | undefined;
|
|
27
|
+
/** Explicit base for resolving a relative `src`. */
|
|
28
|
+
baseUrl?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Fetch one of your OWN bundled documents and get back a `srcdoc` string with
|
|
32
|
+
* an absolute `<base href>` injected — the only way a block can embed a nested
|
|
33
|
+
* document of its own.
|
|
34
|
+
*
|
|
35
|
+
* 🔴 PREFER NOT TO NEED THIS. A plain `<iframe src="/game/index.html">` of your
|
|
36
|
+
* own bundle **cannot load**, and no manifest change fixes it. An engine build
|
|
37
|
+
* (Defold, Unity, Phaser) is a `<canvas>` plus a JS loader and normally mounts
|
|
38
|
+
* directly into the block's own document, which avoids the whole problem class;
|
|
39
|
+
* reach for this hook only when a separate document is genuinely required.
|
|
40
|
+
*
|
|
41
|
+
* WHY, measured, in ONE place: the header of `@civitai/app-sdk`'s
|
|
42
|
+
* `src/blocks/nestedDocument.ts`. It carries the opaque-origin /
|
|
43
|
+
* `frame-ancestors` / `X-Frame-Options` derivation, the five-row DATED
|
|
44
|
+
* EXTERNAL platform matrix (verified by nothing in this repo — re-measure it if
|
|
45
|
+
* the platform's sandbox tiers or response headers change), and the known
|
|
46
|
+
* limits of the `<base>` rewrite. Do not restate any of it here: it was
|
|
47
|
+
* duplicated across 11 surfaces with nothing in the repo checking them for
|
|
48
|
+
* agreement, which is exactly how two measured claims in these files' own test
|
|
49
|
+
* headers went stale.
|
|
50
|
+
*
|
|
51
|
+
* 🔴 THE STRING IS NOT SANITIZED. It is your own markup and scripts, verbatim;
|
|
52
|
+
* pass a `src` you control, and put a `sandbox` attribute on the iframe.
|
|
53
|
+
*
|
|
54
|
+
* Changing `src` restarts the fetch and aborts the previous one — `status` goes
|
|
55
|
+
* back to `'loading'` in the SAME render that changes `src`, so no paint ever
|
|
56
|
+
* shows a stale `srcDoc` as `'ready'` beside a new `src`. Unmounting aborts the
|
|
57
|
+
* in-flight fetch, and no state is written after it.
|
|
58
|
+
*
|
|
59
|
+
* A fetch that never settles is bounded: after {@link NESTED_DOCUMENT_TIMEOUT_MS}
|
|
60
|
+
* it is aborted and `status` becomes `'error'` with a message naming the
|
|
61
|
+
* timeout. A timeout is distinguished from the hook's own aborts (unmount, a
|
|
62
|
+
* `src` change) — those stay silent, as they must.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* const { srcDoc, status, error } = useNestedDocument({ src: '/game/index.html' });
|
|
67
|
+
* if (status === 'error') return <p>Could not load the game: {error?.message}</p>;
|
|
68
|
+
* if (status !== 'ready') return <p>Loading…</p>;
|
|
69
|
+
* return <iframe title="game" sandbox="allow-scripts" srcDoc={srcDoc ?? undefined} />;
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export declare function useNestedDocument(options?: UseNestedDocumentOptions): UseNestedDocument;
|
|
73
|
+
//# sourceMappingURL=useNestedDocument.d.ts.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { fetchNestedDocument } from '@civitai/app-sdk/blocks';
|
|
3
|
+
const IDLE = { status: 'idle', srcDoc: null, error: null };
|
|
4
|
+
const LOADING = { status: 'loading', srcDoc: null, error: null };
|
|
5
|
+
/**
|
|
6
|
+
* How long the fetch is given before it is aborted and the hook reports an
|
|
7
|
+
* error. Without a deadline a stalled response leaves `status === 'loading'`
|
|
8
|
+
* FOREVER: the hook owns its `AbortController` and never exposes it, so a
|
|
9
|
+
* caller has nothing to cancel with and no timer of its own to hang one off.
|
|
10
|
+
*
|
|
11
|
+
* 30 seconds, equal to `transport/requestTimeouts.ts`'s
|
|
12
|
+
* `DEFAULT_REQUEST_TIMEOUT_MS` and to `useEntitlements`' `ENTITLEMENTS_TIMEOUT_MS`
|
|
13
|
+
* — this is a `'protocol'`-class wait in that file's vocabulary: a static-host
|
|
14
|
+
* round trip with NO PERSON IN THE LOOP, so `HUMAN_INTERACTION_TIMEOUT_MS` is
|
|
15
|
+
* the wrong bound. Declared locally rather than imported for the same reason
|
|
16
|
+
* `useEntitlements` declares its own: `DEFAULT_REQUEST_TIMEOUT_MS` documents the
|
|
17
|
+
* default of `IframeTransport.sendRequest`, and a `fetch` of a bundled document
|
|
18
|
+
* is not a bridge request. Equal by intent, not derived.
|
|
19
|
+
*/
|
|
20
|
+
const NESTED_DOCUMENT_TIMEOUT_MS = 30_000;
|
|
21
|
+
/**
|
|
22
|
+
* Fetch one of your OWN bundled documents and get back a `srcdoc` string with
|
|
23
|
+
* an absolute `<base href>` injected — the only way a block can embed a nested
|
|
24
|
+
* document of its own.
|
|
25
|
+
*
|
|
26
|
+
* 🔴 PREFER NOT TO NEED THIS. A plain `<iframe src="/game/index.html">` of your
|
|
27
|
+
* own bundle **cannot load**, and no manifest change fixes it. An engine build
|
|
28
|
+
* (Defold, Unity, Phaser) is a `<canvas>` plus a JS loader and normally mounts
|
|
29
|
+
* directly into the block's own document, which avoids the whole problem class;
|
|
30
|
+
* reach for this hook only when a separate document is genuinely required.
|
|
31
|
+
*
|
|
32
|
+
* WHY, measured, in ONE place: the header of `@civitai/app-sdk`'s
|
|
33
|
+
* `src/blocks/nestedDocument.ts`. It carries the opaque-origin /
|
|
34
|
+
* `frame-ancestors` / `X-Frame-Options` derivation, the five-row DATED
|
|
35
|
+
* EXTERNAL platform matrix (verified by nothing in this repo — re-measure it if
|
|
36
|
+
* the platform's sandbox tiers or response headers change), and the known
|
|
37
|
+
* limits of the `<base>` rewrite. Do not restate any of it here: it was
|
|
38
|
+
* duplicated across 11 surfaces with nothing in the repo checking them for
|
|
39
|
+
* agreement, which is exactly how two measured claims in these files' own test
|
|
40
|
+
* headers went stale.
|
|
41
|
+
*
|
|
42
|
+
* 🔴 THE STRING IS NOT SANITIZED. It is your own markup and scripts, verbatim;
|
|
43
|
+
* pass a `src` you control, and put a `sandbox` attribute on the iframe.
|
|
44
|
+
*
|
|
45
|
+
* Changing `src` restarts the fetch and aborts the previous one — `status` goes
|
|
46
|
+
* back to `'loading'` in the SAME render that changes `src`, so no paint ever
|
|
47
|
+
* shows a stale `srcDoc` as `'ready'` beside a new `src`. Unmounting aborts the
|
|
48
|
+
* in-flight fetch, and no state is written after it.
|
|
49
|
+
*
|
|
50
|
+
* A fetch that never settles is bounded: after {@link NESTED_DOCUMENT_TIMEOUT_MS}
|
|
51
|
+
* it is aborted and `status` becomes `'error'` with a message naming the
|
|
52
|
+
* timeout. A timeout is distinguished from the hook's own aborts (unmount, a
|
|
53
|
+
* `src` change) — those stay silent, as they must.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* const { srcDoc, status, error } = useNestedDocument({ src: '/game/index.html' });
|
|
58
|
+
* if (status === 'error') return <p>Could not load the game: {error?.message}</p>;
|
|
59
|
+
* if (status !== 'ready') return <p>Loading…</p>;
|
|
60
|
+
* return <iframe title="game" sandbox="allow-scripts" srcDoc={srcDoc ?? undefined} />;
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function useNestedDocument(options) {
|
|
64
|
+
const src = options?.src;
|
|
65
|
+
const baseUrl = options?.baseUrl;
|
|
66
|
+
const [state, setState] = useState(src ? LOADING : IDLE);
|
|
67
|
+
// 🔴 RESET DURING RENDER, NOT IN THE EFFECT. The effect runs AFTER the commit,
|
|
68
|
+
// so between the render that changes `src` and the effect that sets `LOADING`
|
|
69
|
+
// the hook used to return the PREVIOUS document still labelled `'ready'` —
|
|
70
|
+
// one painted frame in which `(src, srcDoc)` disagree, and a consumer that
|
|
71
|
+
// keys an iframe off `src` would show the wrong document in it. This is
|
|
72
|
+
// React's documented "adjust state when a prop changes" pattern: the
|
|
73
|
+
// `setState` below re-renders this component immediately, before the browser
|
|
74
|
+
// paints, so the stale pair is never committed.
|
|
75
|
+
//
|
|
76
|
+
// The key carries BOTH inputs because both are effect dependencies. They are
|
|
77
|
+
// joined by a NUL rather than by a printable character: two distinct
|
|
78
|
+
// `(src, baseUrl)` pairs can only collide on one key if the separator itself
|
|
79
|
+
// occurs inside one of them, and a NUL is the byte least likely to appear in
|
|
80
|
+
// a URL anyone passes here. A collision would mean a missed reset, not a
|
|
81
|
+
// wrong document — the effect's own `[src, baseUrl]` deps still re-run it.
|
|
82
|
+
const key = `${src ?? ''}\u0000${baseUrl ?? ''}`;
|
|
83
|
+
const [stateKey, setStateKey] = useState(key);
|
|
84
|
+
let current = state;
|
|
85
|
+
if (stateKey !== key) {
|
|
86
|
+
setStateKey(key);
|
|
87
|
+
// 🔴 RETURNED FROM THIS PASS, not merely queued. React discards the JSX of
|
|
88
|
+
// a render that calls `setState` and retries, so queueing alone already
|
|
89
|
+
// keeps the stale pair off the screen — but it would still be the value
|
|
90
|
+
// this call RETURNS, which is the hook's contract and the thing a test can
|
|
91
|
+
// observe. Assigning locally makes every render consistent, committed or
|
|
92
|
+
// not.
|
|
93
|
+
current = src ? LOADING : IDLE;
|
|
94
|
+
// `IDLE`/`LOADING` are module constants, so when the effect sets the same
|
|
95
|
+
// one a moment later React bails out on reference equality.
|
|
96
|
+
setState(current);
|
|
97
|
+
}
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!src) {
|
|
100
|
+
setState(IDLE);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// `cancelled` is belt AND braces alongside `abort()`: an abort only rejects
|
|
104
|
+
// a fetch that is still in flight, so a response that has already resolved
|
|
105
|
+
// can still land in a `.then` after unmount. Both are needed.
|
|
106
|
+
let cancelled = false;
|
|
107
|
+
// 🔴 A SEPARATE FLAG, NOT `controller.signal.aborted`. The timeout aborts
|
|
108
|
+
// THIS controller, and the `.catch` below drops anything whose signal is
|
|
109
|
+
// aborted — so a timeout wired to the same controller would be SWALLOWED
|
|
110
|
+
// and the hook would sit on `'loading'` exactly as it did with no timeout
|
|
111
|
+
// at all. The flag is what tells "the deadline passed" (report it) from
|
|
112
|
+
// "we unmounted / the src changed" (stay silent).
|
|
113
|
+
let timedOut = false;
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
const timeoutId = setTimeout(() => {
|
|
116
|
+
timedOut = true;
|
|
117
|
+
controller.abort();
|
|
118
|
+
}, NESTED_DOCUMENT_TIMEOUT_MS);
|
|
119
|
+
setState(LOADING);
|
|
120
|
+
fetchNestedDocument({ src, baseUrl, signal: controller.signal })
|
|
121
|
+
.then((doc) => {
|
|
122
|
+
if (cancelled)
|
|
123
|
+
return;
|
|
124
|
+
setState({ status: 'ready', srcDoc: doc.srcDoc, error: null });
|
|
125
|
+
})
|
|
126
|
+
.catch((err) => {
|
|
127
|
+
if (cancelled)
|
|
128
|
+
return;
|
|
129
|
+
if (timedOut) {
|
|
130
|
+
setState({
|
|
131
|
+
status: 'error',
|
|
132
|
+
srcDoc: null,
|
|
133
|
+
error: new Error(`useNestedDocument: fetching ${src} timed out after ` +
|
|
134
|
+
`${NESTED_DOCUMENT_TIMEOUT_MS}ms.`),
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Any OTHER abort is this hook's own cleanup, never something to
|
|
139
|
+
// surface: the component is gone, or a newer `src` already owns the
|
|
140
|
+
// state.
|
|
141
|
+
if (controller.signal.aborted)
|
|
142
|
+
return;
|
|
143
|
+
setState({
|
|
144
|
+
status: 'error',
|
|
145
|
+
srcDoc: null,
|
|
146
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
147
|
+
});
|
|
148
|
+
})
|
|
149
|
+
.finally(() => {
|
|
150
|
+
clearTimeout(timeoutId);
|
|
151
|
+
});
|
|
152
|
+
return () => {
|
|
153
|
+
cancelled = true;
|
|
154
|
+
clearTimeout(timeoutId);
|
|
155
|
+
controller.abort();
|
|
156
|
+
};
|
|
157
|
+
}, [src, baseUrl]);
|
|
158
|
+
return current;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=useNestedDocument.js.map
|
|
@@ -49,6 +49,12 @@ export interface UsePublishGenerationOutputs {
|
|
|
49
49
|
* a human the request carries {@link HUMAN_INTERACTION_TIMEOUT_MS}, not the
|
|
50
50
|
* default protocol timeout.
|
|
51
51
|
*
|
|
52
|
+
* `dev:live` refuses this bridge: publishing requires the viewer's signed-in
|
|
53
|
+
* civitai.com session, which the local harness does not have, so `publish()`
|
|
54
|
+
* rejects with a message saying so. Test publishing against the mock host —
|
|
55
|
+
* `createMockHost` or `Harness` from `@civitai/blocks-react/testing`, with the
|
|
56
|
+
* `publishImageIds` / `publishError` options.
|
|
57
|
+
*
|
|
52
58
|
* @example
|
|
53
59
|
* const { publish } = usePublishGenerationOutputs();
|
|
54
60
|
* const imageIds = await publish({ workflowId: w.workflowId, imageIndexes: [0, 2] });
|
|
@@ -16,6 +16,12 @@ import { sendTypedRequest } from '../transport/transport.js';
|
|
|
16
16
|
* a human the request carries {@link HUMAN_INTERACTION_TIMEOUT_MS}, not the
|
|
17
17
|
* default protocol timeout.
|
|
18
18
|
*
|
|
19
|
+
* `dev:live` refuses this bridge: publishing requires the viewer's signed-in
|
|
20
|
+
* civitai.com session, which the local harness does not have, so `publish()`
|
|
21
|
+
* rejects with a message saying so. Test publishing against the mock host —
|
|
22
|
+
* `createMockHost` or `Harness` from `@civitai/blocks-react/testing`, with the
|
|
23
|
+
* `publishImageIds` / `publishError` options.
|
|
24
|
+
*
|
|
19
25
|
* @example
|
|
20
26
|
* const { publish } = usePublishGenerationOutputs();
|
|
21
27
|
* const imageIds = await publish({ workflowId: w.workflowId, imageIndexes: [0, 2] });
|
package/dist/index.d.ts
CHANGED
|
@@ -98,6 +98,8 @@ export type { SfwGateProps } from './hooks/SfwGate.js';
|
|
|
98
98
|
export { useDirectLoad } from './hooks/useDirectLoad.js';
|
|
99
99
|
export type { UseDirectLoad, UseDirectLoadOptions } from './hooks/useDirectLoad.js';
|
|
100
100
|
export { hostToRunUrl, DIRECT_LOAD_TIMEOUT_MS } from './transport/directLoad.js';
|
|
101
|
+
export { useNestedDocument } from './hooks/useNestedDocument.js';
|
|
102
|
+
export type { NestedDocumentStatus, UseNestedDocument, UseNestedDocumentOptions, } from './hooks/useNestedDocument.js';
|
|
101
103
|
export { useAppStorage } from './hooks/useAppStorage.js';
|
|
102
104
|
export type { AppStorageKeyEntry, AppStorageListResult, AppStorageQuota, UseAppStorage, } from './hooks/useAppStorage.js';
|
|
103
105
|
export { useSharedStorage } from './hooks/useSharedStorage.js';
|
package/dist/index.js
CHANGED
|
@@ -76,6 +76,10 @@ export { useDomainMaturity } from './hooks/useDomainMaturity.js';
|
|
|
76
76
|
export { SfwGate } from './hooks/SfwGate.js';
|
|
77
77
|
export { useDirectLoad } from './hooks/useDirectLoad.js';
|
|
78
78
|
export { hostToRunUrl, DIRECT_LOAD_TIMEOUT_MS } from './transport/directLoad.js';
|
|
79
|
+
// A block CANNOT frame its own bundled content by URL. This is the `srcdoc`
|
|
80
|
+
// fallback; prefer a single-document design. Why, measured, in one place:
|
|
81
|
+
// `@civitai/app-sdk`'s `src/blocks/nestedDocument.ts` header.
|
|
82
|
+
export { useNestedDocument } from './hooks/useNestedDocument.js';
|
|
79
83
|
export { useAppStorage } from './hooks/useAppStorage.js';
|
|
80
84
|
export { useSharedStorage } from './hooks/useSharedStorage.js';
|
|
81
85
|
//# sourceMappingURL=index.js.map
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
* --- BEGIN DERIVED ---
|
|
47
47
|
* fetch chokepoints ..... 2
|
|
48
48
|
* REST endpoints ........ GET /api/v1/blocks/me
|
|
49
|
-
* tRPC procedures .......
|
|
50
|
-
* switch case labels .... 45, of which
|
|
49
|
+
* tRPC procedures ....... 28 = blocks.* (13) + apps.shared.* (10) + apps.storage.* (5)
|
|
50
|
+
* switch case labels .... 45, of which 8 REFUSE (enumerated under SCOPE below)
|
|
51
51
|
* --- END DERIVED ---
|
|
52
52
|
*
|
|
53
53
|
* PICKERS (Phase 1 of "make dev:live a faithful local host"): the live host
|
|
@@ -101,6 +101,11 @@
|
|
|
101
101
|
* host-chrome consent. Replies `collection-unavailable`.
|
|
102
102
|
* • CREATE_POST_FROM_APP — needs the server-resolved preview plus host-chrome
|
|
103
103
|
* confirm before a PUBLIC post is written. Replies with a refusal.
|
|
104
|
+
* • PUBLISH_GENERATION_OUTPUTS — publishing requires the viewer's signed-in
|
|
105
|
+
* civitai.com session, which the local harness does not have; test
|
|
106
|
+
* publishing against the mock host — `createMockHost` or `Harness` from
|
|
107
|
+
* `@civitai/blocks-react/testing`, with the `publishImageIds` /
|
|
108
|
+
* `publishError` options. Replies with a refusal.
|
|
104
109
|
* • GET_WILDCARD_PACK — needs the session-authed resolve plus the in-tab
|
|
105
110
|
* zip/yaml parse that lives in civitai, not this SDK. Replies `parse-failed`.
|
|
106
111
|
* • OPEN_IMAGE_UPLOAD — needs the host's native modal + session-authed byte
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
* --- BEGIN DERIVED ---
|
|
47
47
|
* fetch chokepoints ..... 2
|
|
48
48
|
* REST endpoints ........ GET /api/v1/blocks/me
|
|
49
|
-
* tRPC procedures .......
|
|
50
|
-
* switch case labels .... 45, of which
|
|
49
|
+
* tRPC procedures ....... 28 = blocks.* (13) + apps.shared.* (10) + apps.storage.* (5)
|
|
50
|
+
* switch case labels .... 45, of which 8 REFUSE (enumerated under SCOPE below)
|
|
51
51
|
* --- END DERIVED ---
|
|
52
52
|
*
|
|
53
53
|
* PICKERS (Phase 1 of "make dev:live a faithful local host"): the live host
|
|
@@ -101,6 +101,11 @@
|
|
|
101
101
|
* host-chrome consent. Replies `collection-unavailable`.
|
|
102
102
|
* • CREATE_POST_FROM_APP — needs the server-resolved preview plus host-chrome
|
|
103
103
|
* confirm before a PUBLIC post is written. Replies with a refusal.
|
|
104
|
+
* • PUBLISH_GENERATION_OUTPUTS — publishing requires the viewer's signed-in
|
|
105
|
+
* civitai.com session, which the local harness does not have; test
|
|
106
|
+
* publishing against the mock host — `createMockHost` or `Harness` from
|
|
107
|
+
* `@civitai/blocks-react/testing`, with the `publishImageIds` /
|
|
108
|
+
* `publishError` options. Replies with a refusal.
|
|
104
109
|
* • GET_WILDCARD_PACK — needs the session-authed resolve plus the in-tab
|
|
105
110
|
* zip/yaml parse that lives in civitai, not this SDK. Replies `parse-failed`.
|
|
106
111
|
* • OPEN_IMAGE_UPLOAD — needs the host's native modal + session-authed byte
|
|
@@ -1025,29 +1030,37 @@ export function createLiveHost(options) {
|
|
|
1025
1030
|
return;
|
|
1026
1031
|
}
|
|
1027
1032
|
case 'PUBLISH_GENERATION_OUTPUTS': {
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
//
|
|
1033
|
+
// UNSUPPORTED in live, for the same reason as CREATE_POST_FROM_APP
|
|
1034
|
+
// above: publishing requires the viewer's signed-in civitai.com
|
|
1035
|
+
// session, which the local harness does not have — it holds a dev
|
|
1036
|
+
// block token and has no civitai chrome to render the publish
|
|
1037
|
+
// confirm in. On civitai.com the real host resolves the workflow's
|
|
1038
|
+
// outputs, shows that confirm, and only then publishes bare,
|
|
1039
|
+
// real-scanned public Image rows.
|
|
1040
|
+
//
|
|
1041
|
+
// FREE TEXT, like the post refusal: none of the host's codes is
|
|
1042
|
+
// honest about "this harness cannot do it". `usePublishGenerationOutputs`
|
|
1043
|
+
// rejects with it as the message. Test publishing against the mock
|
|
1044
|
+
// host — `createMockHost` or `Harness` from
|
|
1045
|
+
// `@civitai/blocks-react/testing`, with the `publishImageIds` /
|
|
1046
|
+
// `publishError` options.
|
|
1035
1047
|
if (!isRoutableRequestId(requestId))
|
|
1036
1048
|
return;
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1049
|
+
logOnce('publish-outputs', 'PUBLISH_GENERATION_OUTPUTS is not supported in dev:live (publishing requires the ' +
|
|
1050
|
+
"viewer's signed-in civitai.com session, which the local harness does not have). " +
|
|
1051
|
+
'Replying with a refusal. Test publishing against the mock host — createMockHost ' +
|
|
1052
|
+
'or Harness from @civitai/blocks-react/testing, with the publishImageIds / ' +
|
|
1053
|
+
'publishError options.');
|
|
1054
|
+
dispatchToBlock({
|
|
1055
|
+
type: 'PUBLISH_RESULT',
|
|
1056
|
+
payload: {
|
|
1057
|
+
requestId,
|
|
1058
|
+
error: 'publishing generation outputs is not supported in dev:live — publishing ' +
|
|
1059
|
+
"requires the viewer's signed-in civitai.com session, which the local harness " +
|
|
1060
|
+
'does not have; test publishing against the mock host — createMockHost or ' +
|
|
1061
|
+
'Harness from @civitai/blocks-react/testing, with the publishImageIds / ' +
|
|
1062
|
+
'publishError options',
|
|
1063
|
+
},
|
|
1051
1064
|
});
|
|
1052
1065
|
return;
|
|
1053
1066
|
}
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* called). Only the postMessage bridge round-trips are exercised. Never import
|
|
38
38
|
* this from production code.
|
|
39
39
|
*/
|
|
40
|
-
import { type BlockContext, type BlockResourceInfo, type BlockResourcePickerType, type BlockUploadedImageInfo, type BlockGenerationSourceImageInfo, type BuzzAccountType, type BlockBuzzTransaction, type BlockBuzzAccount, type BlockDailyCompensationResource, type BlockViewer, type BlockWildcardPack, type BlockWildcardPackErrorCode, type AppWorkflow, type BlockGatedImage, type BlockCollectionFollowErrorCode, type BlockCreatePostHostError, type BlockCreatePostResult, type ColorDomain, type SharedStorageValue, type Theme, type ViewerInfo, type WorkflowBody } from '@civitai/app-sdk/blocks';
|
|
40
|
+
import { type BlockContext, type BlockResourceInfo, type BlockResourcePickerType, type BlockUploadedImageInfo, type BlockGenerationSourceImageInfo, type BuzzAccountType, type BlockBuzzTransaction, type BlockBuzzAccount, type BlockDailyCompensationResource, type BlockViewer, type BlockWildcardPack, type BlockWildcardPackErrorCode, type AppWorkflow, type BlockPublishedModel, type BlockGatedImage, type BlockCollectionFollowErrorCode, type BlockCreatePostHostError, type BlockCreatePostResult, type ColorDomain, type SharedStorageValue, type Theme, type ViewerInfo, type WorkflowBody } from '@civitai/app-sdk/blocks';
|
|
41
41
|
/**
|
|
42
42
|
* How submits behave. `'none'` = everything succeeds; `'all'` / `'insufficient'`
|
|
43
43
|
* = every submit RESOLVES an insufficient-Buzz `failed` snapshot, priced
|
|
@@ -178,6 +178,38 @@ export interface MockGenerationScenario {
|
|
|
178
178
|
* {@link image} when both are set.
|
|
179
179
|
*/
|
|
180
180
|
images?: string[] | ((req: WorkflowBody) => string[]);
|
|
181
|
+
/**
|
|
182
|
+
* How many trained epochs a succeeded PASS-THROUGH training step reports on
|
|
183
|
+
* {@link BlockWorkflowSnapshot.trainedEpochs} — epochs `1…n`, each tagged with
|
|
184
|
+
* the submitted `$type`. Applies only to a `{ kind: 'step', $type: 'training' }`
|
|
185
|
+
* or `'imageResourceTraining'` body; every other body is unaffected.
|
|
186
|
+
*
|
|
187
|
+
* Default `0`: the field is omitted, as the host omits it for a run that
|
|
188
|
+
* produced no checkpoint. Opt in with `n > 0` to simulate a finished run —
|
|
189
|
+
* it is off by default because a real run on this arm is bounded by the
|
|
190
|
+
* spend rules below, so a default success would promise what the host may
|
|
191
|
+
* not deliver.
|
|
192
|
+
*
|
|
193
|
+
* The mock never fabricates a checkpoint url: the block contract does not
|
|
194
|
+
* include one, so nothing here does either. A training snapshot's `imageUrls` stand for
|
|
195
|
+
* the run's SAMPLE images only (the same {@link image}/{@link images} knobs
|
|
196
|
+
* apply).
|
|
197
|
+
*
|
|
198
|
+
* 🔴 The real host's spend and timeout rules for this arm (see
|
|
199
|
+
* `WorkflowBodyPassThroughStep.maxBuzz` in `@civitai/app-sdk/blocks`) are
|
|
200
|
+
* NOT simulated: the mock neither quotes nor times out, so a success here
|
|
201
|
+
* says nothing about whether your run is quoted, what it costs, or whether
|
|
202
|
+
* it finishes.
|
|
203
|
+
*/
|
|
204
|
+
trainedEpochs?: number;
|
|
205
|
+
/**
|
|
206
|
+
* When set, a succeeded pass-through training snapshot also carries this as
|
|
207
|
+
* {@link BlockWorkflowSnapshot.publishedModel} — simulating a run the viewer
|
|
208
|
+
* has started (`published: false`) or finished (`published: true`) publishing
|
|
209
|
+
* through the model wizard. Default: unset (field omitted). For the app-queue
|
|
210
|
+
* read, put `publishedModel` on the rows you pass as `appWorkflows`.
|
|
211
|
+
*/
|
|
212
|
+
trainingPublishedModel?: BlockPublishedModel;
|
|
181
213
|
}
|
|
182
214
|
/**
|
|
183
215
|
* BUZZ scenario controls — simulate a balance so the insufficient-Buzz / top-up
|
|
@@ -112,6 +112,15 @@ const BUDGETED_SCOPE = 'ai:write:budgeted';
|
|
|
112
112
|
const DEFAULT_STORAGE_QUOTA_BYTES = APP_STORAGE_MAX_BYTES;
|
|
113
113
|
const DEFAULT_STORAGE_VALUE_CAP_BYTES = APP_STORAGE_MAX_VALUE_BYTES;
|
|
114
114
|
const DEFAULT_STORAGE_LIMIT_ROWS = APP_STORAGE_MAX_ROWS;
|
|
115
|
+
/**
|
|
116
|
+
* The pass-through training `$type` a body names, if any. Exact match — the
|
|
117
|
+
* orchestrator's `$type` discriminator is case-sensitive. Only the pass-through
|
|
118
|
+
* arm carries a `$type`, so no separate `kind` narrow is needed.
|
|
119
|
+
*/
|
|
120
|
+
const passThroughTrainingType = (body) => {
|
|
121
|
+
const t = body.$type;
|
|
122
|
+
return t === 'training' || t === 'imageResourceTraining' ? t : undefined;
|
|
123
|
+
};
|
|
115
124
|
// The canned picks carry the WIDENED BlockResourceInfo projection (PR-C) — the
|
|
116
125
|
// public recommended settings a real host now returns — so dev:mock mirrors prod
|
|
117
126
|
// (a picked resource seeds a weight slider + trigger words). Defaults match the
|
|
@@ -894,11 +903,36 @@ export function createMockHost(options = {}) {
|
|
|
894
903
|
...(consentGranted ? { buzzBudget } : {}),
|
|
895
904
|
};
|
|
896
905
|
};
|
|
906
|
+
/**
|
|
907
|
+
* The training-only fields of a succeeded snapshot — see
|
|
908
|
+
* {@link MockGenerationScenario.trainedEpochs}. KIND-FAITHFUL, unlike the
|
|
909
|
+
* rest of the money path: only a pass-through `training` /
|
|
910
|
+
* `imageResourceTraining` body gets them, because only such a step produces
|
|
911
|
+
* a checkpoint. Empty for every other body, so their snapshots are unchanged.
|
|
912
|
+
*/
|
|
913
|
+
const trainingFields = (body) => {
|
|
914
|
+
const $type = passThroughTrainingType(body);
|
|
915
|
+
if (!$type)
|
|
916
|
+
return {};
|
|
917
|
+
const count = Math.max(0, Math.floor(gen.trainedEpochs ?? 0));
|
|
918
|
+
return {
|
|
919
|
+
...(count > 0
|
|
920
|
+
? {
|
|
921
|
+
trainedEpochs: Array.from({ length: count }, (_, i) => ({
|
|
922
|
+
$type,
|
|
923
|
+
epochNumber: i + 1,
|
|
924
|
+
})),
|
|
925
|
+
}
|
|
926
|
+
: {}),
|
|
927
|
+
...(gen.trainingPublishedModel ? { publishedModel: { ...gen.trainingPublishedModel } } : {}),
|
|
928
|
+
};
|
|
929
|
+
};
|
|
897
930
|
const succeededSnapshot = (workflowId) => {
|
|
898
931
|
const wf = workflows.get(workflowId);
|
|
899
932
|
const cost = wf?.cost ?? legacyCost;
|
|
900
933
|
const body = wf?.body ?? {};
|
|
901
934
|
return {
|
|
935
|
+
...trainingFields(body),
|
|
902
936
|
workflowId,
|
|
903
937
|
status: 'succeeded',
|
|
904
938
|
cost: { total: cost },
|
|
@@ -255,8 +255,91 @@ export function isValidWorkflowSnapshot(s) {
|
|
|
255
255
|
return false;
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
|
+
// ---- The host fields mirrored in #524, plus the training-publish pair. ----
|
|
259
|
+
//
|
|
260
|
+
// Same policy as `autoClaim` above: each is OPTIONAL (an older host omits it
|
|
261
|
+
// and the snapshot stays valid), and a PRESENT but malformed value rejects
|
|
262
|
+
// the snapshot, because the block dereferences these fields.
|
|
263
|
+
//
|
|
264
|
+
// 🔴 ONE DELIBERATE DIFFERENCE: the open-ended string unions
|
|
265
|
+
// (`modelSubstitutions[].reason`, `trainedEpochs[].$type`, `toolCalls[].type`)
|
|
266
|
+
// are checked as STRINGS, not against their current members. A rejected snapshot is a dropped
|
|
267
|
+
// poll reply — the block's request hangs to its timeout — so pinning today's
|
|
268
|
+
// members here would turn the host adding a value into a fleet-wide hang in
|
|
269
|
+
// every already-shipped block bundle. The types document that unknown values
|
|
270
|
+
// may arrive.
|
|
271
|
+
if (s.modelSubstitutions !== undefined) {
|
|
272
|
+
if (!Array.isArray(s.modelSubstitutions))
|
|
273
|
+
return false;
|
|
274
|
+
for (const m of s.modelSubstitutions) {
|
|
275
|
+
if (!isObject(m))
|
|
276
|
+
return false;
|
|
277
|
+
if (!isFiniteNumber(m.requested) || !isFiniteNumber(m.applied))
|
|
278
|
+
return false;
|
|
279
|
+
if (typeof m.reason !== 'string')
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (s.textOutputs !== undefined) {
|
|
284
|
+
if (!Array.isArray(s.textOutputs))
|
|
285
|
+
return false;
|
|
286
|
+
if (!s.textOutputs.every((t) => typeof t === 'string'))
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
if (s.textOutputWithheld !== undefined) {
|
|
290
|
+
if (!isObject(s.textOutputWithheld) || typeof s.textOutputWithheld.reason !== 'string') {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (s.toolCalls !== undefined) {
|
|
295
|
+
if (!Array.isArray(s.toolCalls))
|
|
296
|
+
return false;
|
|
297
|
+
for (const c of s.toolCalls) {
|
|
298
|
+
if (!isObject(c))
|
|
299
|
+
return false;
|
|
300
|
+
if (typeof c.id !== 'string' || typeof c.type !== 'string')
|
|
301
|
+
return false;
|
|
302
|
+
if (!isObject(c.function))
|
|
303
|
+
return false;
|
|
304
|
+
if (typeof c.function.name !== 'string' || typeof c.function.arguments !== 'string') {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (s.stepOutputs !== undefined) {
|
|
310
|
+
if (!Array.isArray(s.stepOutputs))
|
|
311
|
+
return false;
|
|
312
|
+
// `output` is `unknown` by contract — only the `$type` label is dereferenced.
|
|
313
|
+
for (const o of s.stepOutputs) {
|
|
314
|
+
if (!isObject(o) || typeof o.$type !== 'string')
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (s.trainedEpochs !== undefined) {
|
|
319
|
+
if (!Array.isArray(s.trainedEpochs))
|
|
320
|
+
return false;
|
|
321
|
+
for (const e of s.trainedEpochs) {
|
|
322
|
+
if (!isObject(e) || typeof e.$type !== 'string')
|
|
323
|
+
return false;
|
|
324
|
+
if (!Number.isInteger(e.epochNumber))
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (s.publishedModel !== undefined && !isValidPublishedModel(s.publishedModel))
|
|
329
|
+
return false;
|
|
258
330
|
return true;
|
|
259
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Shape check for `publishedModel` — shared by {@link isValidWorkflowSnapshot}
|
|
334
|
+
* and the `AppWorkflow` row guard, which carry the same field.
|
|
335
|
+
*/
|
|
336
|
+
function isValidPublishedModel(p) {
|
|
337
|
+
if (!isObject(p))
|
|
338
|
+
return false;
|
|
339
|
+
if (!isFiniteNumber(p.modelId) || !isFiniteNumber(p.modelVersionId))
|
|
340
|
+
return false;
|
|
341
|
+
return typeof p.published === 'boolean';
|
|
342
|
+
}
|
|
260
343
|
/**
|
|
261
344
|
* Host-pushed token rotation. No `requestId` field; the host is the initiator.
|
|
262
345
|
* Carries the same wrapped-token shape as the reply path.
|
|
@@ -823,6 +906,9 @@ function isValidAppWorkflow(w) {
|
|
|
823
906
|
if (img.nsfwLevel !== null && !isFiniteNumber(img.nsfwLevel))
|
|
824
907
|
return false;
|
|
825
908
|
}
|
|
909
|
+
// OPTIONAL — absent on hosts that predate it; checked only when present.
|
|
910
|
+
if (w.publishedModel !== undefined && !isValidPublishedModel(w.publishedModel))
|
|
911
|
+
return false;
|
|
826
912
|
return true;
|
|
827
913
|
}
|
|
828
914
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@civitai/blocks-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.0",
|
|
4
4
|
"description": "React hooks and iframe transport for Civitai Apps. Pairs with @civitai/app-sdk/blocks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"comment-peerDependencies": "This floor is DERIVED, not chosen — the lowest published app-sdk that exports every symbol this package imports. Do not edit it without reading ./PEER_FLOOR.md (in-repo, unpublished) and tests/guards/blocks-react-peer-floor.test.mjs.",
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@civitai/app-sdk": ">=0.
|
|
45
|
+
"@civitai/app-sdk": ">=0.57.0 <1.0.0",
|
|
46
46
|
"react": "^18.0.0 || ^19.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"typescript": "^5.9.2",
|
|
59
59
|
"vite": "^8.0.14",
|
|
60
60
|
"vitest": "^4.1.11",
|
|
61
|
-
"@civitai/app-sdk": "^0.
|
|
61
|
+
"@civitai/app-sdk": "^0.57.0"
|
|
62
62
|
},
|
|
63
63
|
"publishConfig": {
|
|
64
64
|
"access": "public"
|