@civitai/blocks-react 0.74.1 → 0.76.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 +236 -0
- package/dist/hooks/useMediaUpload.d.ts +97 -0
- package/dist/hooks/useMediaUpload.js +108 -0
- package/dist/hooks/useReactions.d.ts +126 -0
- package/dist/hooks/useReactions.js +384 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/internal/liveHost.d.ts +14 -1
- package/dist/internal/liveHost.js +70 -1
- package/dist/internal/mediaUploadBytes.d.ts +169 -0
- package/dist/internal/mediaUploadBytes.js +268 -0
- package/dist/internal/mockHost.d.ts +62 -2
- package/dist/internal/mockHost.js +104 -2
- package/dist/internal/mockHostReactions.d.ts +62 -0
- package/dist/internal/mockHostReactions.js +166 -0
- package/dist/internal/mockHostStepInput.d.ts +62 -0
- package/dist/internal/mockHostStepInput.js +120 -0
- package/dist/internal/mockWorkflowGraph.d.ts +21 -1
- package/dist/internal/mockWorkflowGraph.js +22 -6
- package/dist/internal/stepInputLimits.d.ts +104 -0
- package/dist/internal/stepInputLimits.js +179 -0
- package/dist/testing.d.ts +2 -0
- package/dist/transport/requestTimeouts.js +7 -0
- package/dist/transport/validate.d.ts +35 -0
- package/dist/transport/validate.js +175 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +3 -3
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* whether its `path` exists, and it prices nothing. A body this accepts can
|
|
11
11
|
* still be refused by the real host or by the orchestrator.
|
|
12
12
|
*/
|
|
13
|
+
import { PASS_THROUGH_INPUT_BYTES_MAX } from './stepInputLimits.js';
|
|
13
14
|
/**
|
|
14
15
|
* The bounds the mock enforces. `maxSteps` is a COPY of
|
|
15
16
|
* `@civitai/app-sdk/blocks`'s `BLOCK_WORKFLOW_MAX_STEPS`, deliberately not
|
|
@@ -27,8 +28,11 @@ export const MOCK_WORKFLOW_BOUNDS = {
|
|
|
27
28
|
maxInputDepth: 32,
|
|
28
29
|
/** How deep a `repeat` template may nest another `repeat`. */
|
|
29
30
|
maxRepeatNesting: 4,
|
|
30
|
-
/**
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Serialized bytes of every step `input` plus `arguments`, summed. The host's
|
|
33
|
+
* `WORKFLOW_INPUT_BYTES_MAX`, which is its pass-through cap by definition.
|
|
34
|
+
*/
|
|
35
|
+
maxInputBytes: PASS_THROUGH_INPUT_BYTES_MAX,
|
|
32
36
|
/** Characters of a step `$type`, and of a step `name` on the wire. */
|
|
33
37
|
maxTypeChars: 64,
|
|
34
38
|
/**
|
|
@@ -335,15 +339,19 @@ export function mockUnquotedWorkflowEstimateRefusal(failure) {
|
|
|
335
339
|
errorCode: `workflow_quote_${failure}`,
|
|
336
340
|
};
|
|
337
341
|
}
|
|
338
|
-
/**
|
|
339
|
-
|
|
342
|
+
/**
|
|
343
|
+
* The host's wire schema, mirrored: exact keys, types and sizes — everything
|
|
344
|
+
* but the summed byte cap. Exported so the step-input gate
|
|
345
|
+
* (`mockHostStepInput.ts`) runs its rules only on a body the schema's shape
|
|
346
|
+
* accepts, as zod runs a refinement only after the shape parses.
|
|
347
|
+
*/
|
|
348
|
+
export function isWellFormedWorkflowShape(body) {
|
|
340
349
|
if (!isPlainObject(body) || !Object.keys(body).every((key) => BODY_KEYS.has(key)))
|
|
341
350
|
return false;
|
|
342
351
|
if (!Array.isArray(body.steps) || body.steps.length === 0)
|
|
343
352
|
return false;
|
|
344
353
|
if (body.arguments !== undefined && !isPlainObject(body.arguments))
|
|
345
354
|
return false;
|
|
346
|
-
let bytes = body.arguments ? JSON.stringify(body.arguments).length : 0;
|
|
347
355
|
for (const step of body.steps) {
|
|
348
356
|
if (!isPlainObject(step) || !Object.keys(step).every((key) => STEP_KEYS.has(key)))
|
|
349
357
|
return false;
|
|
@@ -359,8 +367,16 @@ function isWellFormedBody(body) {
|
|
|
359
367
|
return false;
|
|
360
368
|
if (suppressOutput !== undefined && typeof suppressOutput !== 'boolean')
|
|
361
369
|
return false;
|
|
362
|
-
bytes += JSON.stringify(input).length;
|
|
363
370
|
}
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
/** The host's wire schema, mirrored in full: the shape, then the summed byte cap. No `errorCode`. */
|
|
374
|
+
function isWellFormedBody(body) {
|
|
375
|
+
if (!isWellFormedWorkflowShape(body))
|
|
376
|
+
return false;
|
|
377
|
+
let bytes = body.arguments ? JSON.stringify(body.arguments).length : 0;
|
|
378
|
+
for (const step of body.steps)
|
|
379
|
+
bytes += JSON.stringify(step.input).length;
|
|
364
380
|
return bytes <= MOCK_WORKFLOW_BOUNDS.maxInputBytes;
|
|
365
381
|
}
|
|
366
382
|
/**
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host's limits on a PASS-THROUGH step `input`: a `kind: 'step'` body with
|
|
3
|
+
* no `step` id, and every step `input` plus `arguments` of a `kind: 'workflow'`
|
|
4
|
+
* body.
|
|
5
|
+
*
|
|
6
|
+
* 🔴 A VERBATIM PORT. The inline-media half is civitai/civitai
|
|
7
|
+
* `src/server/schema/blocks/inline-media.ts` (constants, regexes, the walk, the
|
|
8
|
+
* path format and the message), and the byte cap is `PASS_THROUGH_INPUT_BYTES_MAX`
|
|
9
|
+
* in `src/server/schema/blocks/workflow.schema.ts`. `test/stepInputLimits.test.ts`
|
|
10
|
+
* pins the values and the message template to the server's literals. Change
|
|
11
|
+
* this file only to follow the server, never to improve on it.
|
|
12
|
+
*
|
|
13
|
+
* Dependency-free on purpose: the mock host uses it, and nothing here may raise
|
|
14
|
+
* this package's `@civitai/app-sdk` peer floor
|
|
15
|
+
* (`tests/guards/blocks-react-peer-floor.test.mjs`).
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Max serialized size of one pass-through step `input`, and the host's summed
|
|
19
|
+
* `WORKFLOW_INPUT_BYTES_MAX` for a `kind: 'workflow'` body (defined equal).
|
|
20
|
+
*
|
|
21
|
+
* 🔴 MEASURED AS THE SERVER MEASURES IT: `JSON.stringify(input).length`, the
|
|
22
|
+
* UTF-16 length of the serialization, NOT its UTF-8 byte count. The server's
|
|
23
|
+
* error says "byte", but a non-ASCII input is counted in code units, so do not
|
|
24
|
+
* "fix" this to `TextEncoder` — that would refuse bodies the host accepts.
|
|
25
|
+
*/
|
|
26
|
+
export declare const PASS_THROUGH_INPUT_BYTES_MAX = 262144;
|
|
27
|
+
export declare const INLINE_MEDIA_DATA_URI_REGEX: RegExp;
|
|
28
|
+
/** Base64 alphabet with optional `=` padding. Line-wrapped (MIME) base64 does not match. */
|
|
29
|
+
export declare const INLINE_MEDIA_RAW_BASE64_REGEX: RegExp;
|
|
30
|
+
/**
|
|
31
|
+
* Above any id, hash or token a step input carries. Raw base64 shorter than this
|
|
32
|
+
* is not refused; only the data-URI clause catches a tiny image.
|
|
33
|
+
*/
|
|
34
|
+
export declare const INLINE_MEDIA_RAW_BASE64_MIN_CHARS = 4096;
|
|
35
|
+
/**
|
|
36
|
+
* Values one body may hold before it is refused outright. A body past it is
|
|
37
|
+
* padding — often properties holding `undefined`, which the byte cap counts as
|
|
38
|
+
* zero bytes — so the search fails CLOSED rather than giving up and accepting.
|
|
39
|
+
*/
|
|
40
|
+
export declare const INLINE_MEDIA_MAX_VISITS = 262144;
|
|
41
|
+
export type InlineMediaPath = (string | number)[];
|
|
42
|
+
/**
|
|
43
|
+
* Shared by every field of one body, so many fields cannot each spend a full
|
|
44
|
+
* budget. `stringChars` is a lower bound on the body's JSON length, counted from
|
|
45
|
+
* the strings the search has passed; `inspected` counts the characters handed to
|
|
46
|
+
* the patterns. `maxVisits` replaces {@link INLINE_MEDIA_MAX_VISITS} as the value
|
|
47
|
+
* budget, for a caller whose own cap needs a smaller or larger one (the step and
|
|
48
|
+
* workflow arms never set it).
|
|
49
|
+
*/
|
|
50
|
+
export type InlineMediaBudget = {
|
|
51
|
+
visits: number;
|
|
52
|
+
stringChars?: number;
|
|
53
|
+
inspected?: number;
|
|
54
|
+
maxVisits?: number;
|
|
55
|
+
};
|
|
56
|
+
/** Returned when the body holds more values than the budget allows ({@link INLINE_MEDIA_MAX_VISITS}). */
|
|
57
|
+
export declare const INLINE_MEDIA_TOO_MANY_VALUES = "too-many-values";
|
|
58
|
+
/**
|
|
59
|
+
* Returned once the strings already passed make the body's JSON longer than the
|
|
60
|
+
* byte limit the caller named. From there no further string is read (the walk
|
|
61
|
+
* still finishes, for the value budget); the caller's byte cap refuses the body.
|
|
62
|
+
* So a photo AFTER a string that already crossed the cap gets the byte-cap
|
|
63
|
+
* message, not the inline-media one.
|
|
64
|
+
*/
|
|
65
|
+
export declare const INLINE_MEDIA_OVER_BYTE_LIMIT = "over-byte-limit";
|
|
66
|
+
export declare function isInlineMediaString(value: string): boolean;
|
|
67
|
+
/** One value in a walk: where it sits (`key` in `parent`, `null` at the root) and what it is. */
|
|
68
|
+
export type JsonWalkFrame = {
|
|
69
|
+
value: unknown;
|
|
70
|
+
key: string | number | null;
|
|
71
|
+
parent: JsonWalkFrame | null;
|
|
72
|
+
};
|
|
73
|
+
/** `'stop'` ends the walk at this value; `'skip'` leaves its children unvisited. */
|
|
74
|
+
export type JsonWalkVisitor = (frame: JsonWalkFrame) => 'stop' | 'skip' | void;
|
|
75
|
+
/**
|
|
76
|
+
* Visits every value in `value` in document order: array items by index, then
|
|
77
|
+
* own enumerable keys in `for…in` order. Each value costs one visit from
|
|
78
|
+
* `budget`, charged when its container is opened, so a container with too many
|
|
79
|
+
* children ends the walk before any of them is visited.
|
|
80
|
+
*
|
|
81
|
+
* Returns the frame `visit` stopped at, `null` once every value is visited, or
|
|
82
|
+
* {@link INLINE_MEDIA_TOO_MANY_VALUES} once `budget` is spent. Iterative, so
|
|
83
|
+
* nesting depth cannot overflow the stack.
|
|
84
|
+
*/
|
|
85
|
+
export declare function walkJsonValues(value: unknown, budget: InlineMediaBudget, visit: JsonWalkVisitor): JsonWalkFrame | null | typeof INLINE_MEDIA_TOO_MANY_VALUES;
|
|
86
|
+
/**
|
|
87
|
+
* The JSON path of the first string in `value`, in document order, that carries
|
|
88
|
+
* inline media; `null` if none; {@link INLINE_MEDIA_TOO_MANY_VALUES} once
|
|
89
|
+
* `budget` is spent.
|
|
90
|
+
*
|
|
91
|
+
* With a `byteLimit`, each string is checked BEFORE its length is counted, so
|
|
92
|
+
* the string that crosses the limit is still searched: an over-limit photo gets
|
|
93
|
+
* this message, not the byte cap's. Once the counted strings pass the limit, no
|
|
94
|
+
* later string is read; the walk still finishes, so a body past the value budget
|
|
95
|
+
* is reported as before, and otherwise the search returns
|
|
96
|
+
* {@link INLINE_MEDIA_OVER_BYTE_LIMIT}. The count is a lower bound on the JSON
|
|
97
|
+
* length (each string's characters and quotes, only where `JSON.stringify`
|
|
98
|
+
* writes it), so it never passes the limit for a body that fits.
|
|
99
|
+
*/
|
|
100
|
+
export declare function findInlineMediaPath(value: unknown, budget?: InlineMediaBudget, byteLimit?: number): InlineMediaPath | null | typeof INLINE_MEDIA_TOO_MANY_VALUES | typeof INLINE_MEDIA_OVER_BYTE_LIMIT;
|
|
101
|
+
/** `['input', 'images', 0]` → `input.images[0]`. */
|
|
102
|
+
export declare function formatInlineMediaPath(path: InlineMediaPath): string;
|
|
103
|
+
export declare function inlineMediaMessage(path: InlineMediaPath): string;
|
|
104
|
+
//# sourceMappingURL=stepInputLimits.d.ts.map
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host's limits on a PASS-THROUGH step `input`: a `kind: 'step'` body with
|
|
3
|
+
* no `step` id, and every step `input` plus `arguments` of a `kind: 'workflow'`
|
|
4
|
+
* body.
|
|
5
|
+
*
|
|
6
|
+
* 🔴 A VERBATIM PORT. The inline-media half is civitai/civitai
|
|
7
|
+
* `src/server/schema/blocks/inline-media.ts` (constants, regexes, the walk, the
|
|
8
|
+
* path format and the message), and the byte cap is `PASS_THROUGH_INPUT_BYTES_MAX`
|
|
9
|
+
* in `src/server/schema/blocks/workflow.schema.ts`. `test/stepInputLimits.test.ts`
|
|
10
|
+
* pins the values and the message template to the server's literals. Change
|
|
11
|
+
* this file only to follow the server, never to improve on it.
|
|
12
|
+
*
|
|
13
|
+
* Dependency-free on purpose: the mock host uses it, and nothing here may raise
|
|
14
|
+
* this package's `@civitai/app-sdk` peer floor
|
|
15
|
+
* (`tests/guards/blocks-react-peer-floor.test.mjs`).
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Max serialized size of one pass-through step `input`, and the host's summed
|
|
19
|
+
* `WORKFLOW_INPUT_BYTES_MAX` for a `kind: 'workflow'` body (defined equal).
|
|
20
|
+
*
|
|
21
|
+
* 🔴 MEASURED AS THE SERVER MEASURES IT: `JSON.stringify(input).length`, the
|
|
22
|
+
* UTF-16 length of the serialization, NOT its UTF-8 byte count. The server's
|
|
23
|
+
* error says "byte", but a non-ASCII input is counted in code units, so do not
|
|
24
|
+
* "fix" this to `TextEncoder` — that would refuse bodies the host accepts.
|
|
25
|
+
*/
|
|
26
|
+
export const PASS_THROUGH_INPUT_BYTES_MAX = 262_144;
|
|
27
|
+
export const INLINE_MEDIA_DATA_URI_REGEX = /^\s*data:(image|video|audio)\//i;
|
|
28
|
+
/** Base64 alphabet with optional `=` padding. Line-wrapped (MIME) base64 does not match. */
|
|
29
|
+
export const INLINE_MEDIA_RAW_BASE64_REGEX = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
30
|
+
/**
|
|
31
|
+
* Above any id, hash or token a step input carries. Raw base64 shorter than this
|
|
32
|
+
* is not refused; only the data-URI clause catches a tiny image.
|
|
33
|
+
*/
|
|
34
|
+
export const INLINE_MEDIA_RAW_BASE64_MIN_CHARS = 4096;
|
|
35
|
+
/**
|
|
36
|
+
* Values one body may hold before it is refused outright. A body past it is
|
|
37
|
+
* padding — often properties holding `undefined`, which the byte cap counts as
|
|
38
|
+
* zero bytes — so the search fails CLOSED rather than giving up and accepting.
|
|
39
|
+
*/
|
|
40
|
+
export const INLINE_MEDIA_MAX_VISITS = 262_144;
|
|
41
|
+
/** Returned when the body holds more values than the budget allows ({@link INLINE_MEDIA_MAX_VISITS}). */
|
|
42
|
+
export const INLINE_MEDIA_TOO_MANY_VALUES = 'too-many-values';
|
|
43
|
+
/**
|
|
44
|
+
* Returned once the strings already passed make the body's JSON longer than the
|
|
45
|
+
* byte limit the caller named. From there no further string is read (the walk
|
|
46
|
+
* still finishes, for the value budget); the caller's byte cap refuses the body.
|
|
47
|
+
* So a photo AFTER a string that already crossed the cap gets the byte-cap
|
|
48
|
+
* message, not the inline-media one.
|
|
49
|
+
*/
|
|
50
|
+
export const INLINE_MEDIA_OVER_BYTE_LIMIT = 'over-byte-limit';
|
|
51
|
+
export function isInlineMediaString(value) {
|
|
52
|
+
if (INLINE_MEDIA_DATA_URI_REGEX.test(value))
|
|
53
|
+
return true;
|
|
54
|
+
return value.length >= INLINE_MEDIA_RAW_BASE64_MIN_CHARS && INLINE_MEDIA_RAW_BASE64_REGEX.test(value);
|
|
55
|
+
}
|
|
56
|
+
function pathOf(frame) {
|
|
57
|
+
const path = [];
|
|
58
|
+
for (let f = frame; f && f.key !== null; f = f.parent)
|
|
59
|
+
path.push(f.key);
|
|
60
|
+
return path.reverse();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Visits every value in `value` in document order: array items by index, then
|
|
64
|
+
* own enumerable keys in `for…in` order. Each value costs one visit from
|
|
65
|
+
* `budget`, charged when its container is opened, so a container with too many
|
|
66
|
+
* children ends the walk before any of them is visited.
|
|
67
|
+
*
|
|
68
|
+
* Returns the frame `visit` stopped at, `null` once every value is visited, or
|
|
69
|
+
* {@link INLINE_MEDIA_TOO_MANY_VALUES} once `budget` is spent. Iterative, so
|
|
70
|
+
* nesting depth cannot overflow the stack.
|
|
71
|
+
*/
|
|
72
|
+
export function walkJsonValues(value, budget, visit) {
|
|
73
|
+
const maxVisits = budget.maxVisits ?? INLINE_MEDIA_MAX_VISITS;
|
|
74
|
+
const stack = [{ value, key: null, parent: null }];
|
|
75
|
+
if (++budget.visits > maxVisits)
|
|
76
|
+
return INLINE_MEDIA_TOO_MANY_VALUES;
|
|
77
|
+
while (stack.length > 0) {
|
|
78
|
+
const frame = stack.pop();
|
|
79
|
+
const verdict = visit(frame);
|
|
80
|
+
if (verdict === 'stop')
|
|
81
|
+
return frame;
|
|
82
|
+
const node = frame.value;
|
|
83
|
+
if (verdict === 'skip' || node === null || typeof node !== 'object')
|
|
84
|
+
continue;
|
|
85
|
+
const children = [];
|
|
86
|
+
if (Array.isArray(node)) {
|
|
87
|
+
for (let i = 0; i < node.length; i++) {
|
|
88
|
+
if (++budget.visits > maxVisits)
|
|
89
|
+
return INLINE_MEDIA_TOO_MANY_VALUES;
|
|
90
|
+
children.push({ value: node[i], key: i, parent: frame });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
for (const key in node) {
|
|
95
|
+
if (!Object.prototype.hasOwnProperty.call(node, key))
|
|
96
|
+
continue;
|
|
97
|
+
if (++budget.visits > maxVisits)
|
|
98
|
+
return INLINE_MEDIA_TOO_MANY_VALUES;
|
|
99
|
+
children.push({ value: node[key], key, parent: frame });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (let i = children.length - 1; i >= 0; i--)
|
|
103
|
+
stack.push(children[i]);
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A container whose own entries `JSON.stringify` writes as they are: an array or
|
|
109
|
+
* a plain object, without `toJSON` (own or inherited) and not a boxed value.
|
|
110
|
+
*/
|
|
111
|
+
function writesOwnEntries(value) {
|
|
112
|
+
if (typeof value.toJSON === 'function')
|
|
113
|
+
return false;
|
|
114
|
+
if (value instanceof String || value instanceof Number || value instanceof Boolean)
|
|
115
|
+
return false;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The JSON path of the first string in `value`, in document order, that carries
|
|
120
|
+
* inline media; `null` if none; {@link INLINE_MEDIA_TOO_MANY_VALUES} once
|
|
121
|
+
* `budget` is spent.
|
|
122
|
+
*
|
|
123
|
+
* With a `byteLimit`, each string is checked BEFORE its length is counted, so
|
|
124
|
+
* the string that crosses the limit is still searched: an over-limit photo gets
|
|
125
|
+
* this message, not the byte cap's. Once the counted strings pass the limit, no
|
|
126
|
+
* later string is read; the walk still finishes, so a body past the value budget
|
|
127
|
+
* is reported as before, and otherwise the search returns
|
|
128
|
+
* {@link INLINE_MEDIA_OVER_BYTE_LIMIT}. The count is a lower bound on the JSON
|
|
129
|
+
* length (each string's characters and quotes, only where `JSON.stringify`
|
|
130
|
+
* writes it), so it never passes the limit for a body that fits.
|
|
131
|
+
*/
|
|
132
|
+
export function findInlineMediaPath(value, budget = { visits: 0 }, byteLimit = Infinity) {
|
|
133
|
+
// Containers whose entries `JSON.stringify` does not write as they are (a
|
|
134
|
+
// boxed value, an object with `toJSON`), and everything under them: their
|
|
135
|
+
// strings are not counted.
|
|
136
|
+
const unwritten = new Set();
|
|
137
|
+
let overLimit = false;
|
|
138
|
+
const hit = walkJsonValues(value, budget, (frame) => {
|
|
139
|
+
const node = frame.value;
|
|
140
|
+
const counted = !(frame.parent && unwritten.has(frame.parent));
|
|
141
|
+
if (node !== null && typeof node === 'object') {
|
|
142
|
+
if (!counted || !writesOwnEntries(node))
|
|
143
|
+
unwritten.add(frame);
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
if (typeof node !== 'string')
|
|
147
|
+
return undefined;
|
|
148
|
+
if ((budget.stringChars ?? 0) > byteLimit) {
|
|
149
|
+
overLimit = true;
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
budget.inspected = (budget.inspected ?? 0) + node.length;
|
|
153
|
+
if (isInlineMediaString(node))
|
|
154
|
+
return 'stop';
|
|
155
|
+
if (counted)
|
|
156
|
+
budget.stringChars = (budget.stringChars ?? 0) + node.length + 2;
|
|
157
|
+
return undefined;
|
|
158
|
+
});
|
|
159
|
+
if (hit === INLINE_MEDIA_TOO_MANY_VALUES)
|
|
160
|
+
return hit;
|
|
161
|
+
if (overLimit)
|
|
162
|
+
return INLINE_MEDIA_OVER_BYTE_LIMIT;
|
|
163
|
+
return hit ? pathOf(hit) : null;
|
|
164
|
+
}
|
|
165
|
+
/** `['input', 'images', 0]` → `input.images[0]`. */
|
|
166
|
+
export function formatInlineMediaPath(path) {
|
|
167
|
+
return path.reduce((out, seg) => {
|
|
168
|
+
if (typeof seg === 'number')
|
|
169
|
+
return `${out}[${seg}]`;
|
|
170
|
+
return out ? `${out}.${seg}` : seg;
|
|
171
|
+
}, '');
|
|
172
|
+
}
|
|
173
|
+
export function inlineMediaMessage(path) {
|
|
174
|
+
return (`${formatInlineMediaPath(path)} carries inline media (data URI/base64); ` +
|
|
175
|
+
`pass a hosted url instead — upload the image, video or audio with useMediaUpload ` +
|
|
176
|
+
`(an image also works with useImageUpload({ purpose: 'generationSource' }) or ` +
|
|
177
|
+
`useImagePicker) and pass the returned url`);
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=stepInputLimits.js.map
|
package/dist/testing.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ import { type MockHostOptions } from './internal/mockHost.js';
|
|
|
22
22
|
export { __resetTransport as resetTransport };
|
|
23
23
|
export { installPopoverShim, type PopoverShimHandle, type PopoverShimOptions, } from './internal/popoverShim.js';
|
|
24
24
|
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 MockWorkflowStepInfo, type MockWorkflowQuoteFailure, type CostSpec, type ImageSpec, type CannedPick, } from './internal/mockHost.js';
|
|
25
|
+
export { type MockMediaUploads } from './internal/mediaUploadBytes.js';
|
|
26
|
+
export { type MockReactionSeed } from './internal/mockHostReactions.js';
|
|
25
27
|
export { fakeResourceSource, type FakeResourceCanned, type FakeResourceSource, } from './internal/fakeResourceSource.js';
|
|
26
28
|
/**
|
|
27
29
|
* Props for the dev {@link Harness}.
|
|
@@ -70,6 +70,7 @@ const REQUEST_TIMEOUT_CLASS = {
|
|
|
70
70
|
OPEN_CHECKPOINT_PICKER: 'human', // viewer browses + picks
|
|
71
71
|
OPEN_IMAGE_PICKER: 'human', // viewer browses their images + picks
|
|
72
72
|
OPEN_IMAGE_UPLOAD: 'human', // viewer chooses a file
|
|
73
|
+
OPEN_MEDIA_UPLOAD: 'human', // viewer picks a file, or a 64 MB upload + host retries
|
|
73
74
|
OPEN_RESOURCE_PICKER: 'human', // viewer browses + picks
|
|
74
75
|
PUBLISH_GENERATION_OUTPUTS: 'human', // viewer answers a consent confirm (#4158)
|
|
75
76
|
// 🔴 `'human'` for the SAME reason as PUBLISH_GENERATION_OUTPUTS, and the
|
|
@@ -159,6 +160,12 @@ const REQUEST_TIMEOUT_CLASS = {
|
|
|
159
160
|
// whether a human is involved. Not reclassified here.
|
|
160
161
|
SAVE_IMAGE: 'protocol',
|
|
161
162
|
SET_USER_CHECKPOINT: 'protocol',
|
|
163
|
+
// App reactions. `'protocol'`, NOT `'human'`, and that is a decision rather
|
|
164
|
+
// than an omission: unlike SET_COLLECTION_FOLLOW there is no host confirm to
|
|
165
|
+
// wait on. The consent is the `social:react:self` grant, made beforehand; a
|
|
166
|
+
// host without it refuses `needs-consent` at once.
|
|
167
|
+
SET_ENTITY_REACTION: 'protocol',
|
|
168
|
+
GET_ENTITY_REACTIONS: 'protocol',
|
|
162
169
|
SHARED_APPEND: 'protocol',
|
|
163
170
|
SHARED_GET: 'protocol',
|
|
164
171
|
SHARED_GET_COUNT: 'protocol',
|
|
@@ -395,6 +395,30 @@ export declare function isValidImagesResult(p: unknown): boolean;
|
|
|
395
395
|
* reach a consumer keying UI state by id.
|
|
396
396
|
*/
|
|
397
397
|
export declare function isValidCollectionFollowResult(p: unknown): boolean;
|
|
398
|
+
/**
|
|
399
|
+
* Reply to a block-initiated `SET_ENTITY_REACTION`. A well-formed reply carries
|
|
400
|
+
* EITHER a `result` OR an `error`; one with neither is malformed and dropped.
|
|
401
|
+
*
|
|
402
|
+
* `error` is SHAPE-CHECKED ONLY, although the host's set is closed
|
|
403
|
+
* (`ENTITY_REACTION_ERROR_CODES`): a code a newer host adds must still reach
|
|
404
|
+
* the block as a rejection. A membership check would drop it, and a dropped
|
|
405
|
+
* reply on a request-style message does not reject, it waits out the timeout.
|
|
406
|
+
* `useReactions` turns an unknown string into a `ReactionError` with no `code`.
|
|
407
|
+
*
|
|
408
|
+
* `result` is checked as far as a consumer reads it: `entityId` a positive
|
|
409
|
+
* integer (state is keyed by it), `reaction` a non-empty string, `reacted` and
|
|
410
|
+
* `changed` booleans, plus the state fields.
|
|
411
|
+
*/
|
|
412
|
+
export declare function isValidEntityReactionResult(p: unknown): boolean;
|
|
413
|
+
/**
|
|
414
|
+
* Reply to a block-initiated `GET_ENTITY_REACTIONS`. Same one-of and shape-only
|
|
415
|
+
* `error` contract as {@link isValidEntityReactionResult}.
|
|
416
|
+
*
|
|
417
|
+
* `result.byId` must be an object whose every key is a positive-integer id
|
|
418
|
+
* spelled as a string and whose every value is a valid state;
|
|
419
|
+
* `result.unavailable` an array of positive-integer ids.
|
|
420
|
+
*/
|
|
421
|
+
export declare function isValidEntityReactionsResult(p: unknown): boolean;
|
|
398
422
|
/**
|
|
399
423
|
* Reply to a block-initiated `CREATE_POST_FROM_APP`. A well-formed reply carries
|
|
400
424
|
* EITHER a `result` (`{ postId: positive int, url: non-empty string, imageIds:
|
|
@@ -581,6 +605,17 @@ export declare function isValidResourcePickerResult(p: unknown): boolean;
|
|
|
581
605
|
* out its whole human-interaction timeout on a refusal the host already sent.
|
|
582
606
|
*/
|
|
583
607
|
export declare function isValidImagePickerResult(p: unknown): boolean;
|
|
608
|
+
/**
|
|
609
|
+
* Reply to `OPEN_MEDIA_UPLOAD`: `{ requestId, selected }`, `{ requestId, error }`
|
|
610
|
+
* or a bare `{ requestId }` (dismissed / declined).
|
|
611
|
+
*
|
|
612
|
+
* `error` must be a string when present, and a reply carrying one is accepted
|
|
613
|
+
* without looking further: `useMediaUpload` throws on a PRESENT `error` before
|
|
614
|
+
* it reads `selected`. 🔴 Accepting it is load-bearing: dropping
|
|
615
|
+
* `{ requestId, error }` would leave the hook waiting out its whole
|
|
616
|
+
* human-interaction timeout on a refusal the host already sent.
|
|
617
|
+
*/
|
|
618
|
+
export declare function isValidMediaUploadResult(p: unknown): boolean;
|
|
584
619
|
/**
|
|
585
620
|
* Reply to `SET_USER_CHECKPOINT`. `ok` is required on the success path; the
|
|
586
621
|
* hook throws on `!ok || error !== undefined` — presence, not truthiness, so an
|
|
@@ -1368,6 +1368,121 @@ export function isValidCollectionFollowResult(p) {
|
|
|
1368
1368
|
return false;
|
|
1369
1369
|
return true;
|
|
1370
1370
|
}
|
|
1371
|
+
/** A positive integer id: what a reaction reply keys UI state by. */
|
|
1372
|
+
function isPositiveIntId(v) {
|
|
1373
|
+
return typeof v === 'number' && Number.isInteger(v) && v > 0;
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* One image's reaction state inside an `ENTITY_REACTION(S)_RESULT`: `counts` an
|
|
1377
|
+
* object of finite numbers, `viewerReactions` an array of strings,
|
|
1378
|
+
* `statsUnknown` a boolean.
|
|
1379
|
+
*
|
|
1380
|
+
* 🔴 The reaction NAMES are not membership-checked, in `counts` or in
|
|
1381
|
+
* `viewerReactions`. The site stores five reactions and may add a sixth; a
|
|
1382
|
+
* reply naming one this SDK does not know is still a true statement about the
|
|
1383
|
+
* viewer, and dropping it would hang the request to its timeout instead.
|
|
1384
|
+
*/
|
|
1385
|
+
function isValidEntityReactionState(v) {
|
|
1386
|
+
if (!isObject(v))
|
|
1387
|
+
return false;
|
|
1388
|
+
if (!isObject(v.counts))
|
|
1389
|
+
return false;
|
|
1390
|
+
for (const n of Object.values(v.counts)) {
|
|
1391
|
+
if (!isFiniteNumber(n))
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
if (!Array.isArray(v.viewerReactions))
|
|
1395
|
+
return false;
|
|
1396
|
+
for (const r of v.viewerReactions) {
|
|
1397
|
+
if (typeof r !== 'string')
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
if (typeof v.statsUnknown !== 'boolean')
|
|
1401
|
+
return false;
|
|
1402
|
+
return true;
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Reply to a block-initiated `SET_ENTITY_REACTION`. A well-formed reply carries
|
|
1406
|
+
* EITHER a `result` OR an `error`; one with neither is malformed and dropped.
|
|
1407
|
+
*
|
|
1408
|
+
* `error` is SHAPE-CHECKED ONLY, although the host's set is closed
|
|
1409
|
+
* (`ENTITY_REACTION_ERROR_CODES`): a code a newer host adds must still reach
|
|
1410
|
+
* the block as a rejection. A membership check would drop it, and a dropped
|
|
1411
|
+
* reply on a request-style message does not reject, it waits out the timeout.
|
|
1412
|
+
* `useReactions` turns an unknown string into a `ReactionError` with no `code`.
|
|
1413
|
+
*
|
|
1414
|
+
* `result` is checked as far as a consumer reads it: `entityId` a positive
|
|
1415
|
+
* integer (state is keyed by it), `reaction` a non-empty string, `reacted` and
|
|
1416
|
+
* `changed` booleans, plus the state fields.
|
|
1417
|
+
*/
|
|
1418
|
+
export function isValidEntityReactionResult(p) {
|
|
1419
|
+
if (!isObject(p))
|
|
1420
|
+
return false;
|
|
1421
|
+
if (!isWireRequestIdShape(p.requestId))
|
|
1422
|
+
return false;
|
|
1423
|
+
if (p.error !== undefined && typeof p.error !== 'string')
|
|
1424
|
+
return false;
|
|
1425
|
+
if (p.result !== undefined) {
|
|
1426
|
+
const r = p.result;
|
|
1427
|
+
if (!isObject(r))
|
|
1428
|
+
return false;
|
|
1429
|
+
if (!isNonEmptyString(r.entityType))
|
|
1430
|
+
return false;
|
|
1431
|
+
if (!isPositiveIntId(r.entityId))
|
|
1432
|
+
return false;
|
|
1433
|
+
if (!isNonEmptyString(r.reaction))
|
|
1434
|
+
return false;
|
|
1435
|
+
if (typeof r.reacted !== 'boolean')
|
|
1436
|
+
return false;
|
|
1437
|
+
if (typeof r.changed !== 'boolean')
|
|
1438
|
+
return false;
|
|
1439
|
+
if (!isValidEntityReactionState(r))
|
|
1440
|
+
return false;
|
|
1441
|
+
}
|
|
1442
|
+
if (p.result === undefined && p.error === undefined)
|
|
1443
|
+
return false;
|
|
1444
|
+
return true;
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Reply to a block-initiated `GET_ENTITY_REACTIONS`. Same one-of and shape-only
|
|
1448
|
+
* `error` contract as {@link isValidEntityReactionResult}.
|
|
1449
|
+
*
|
|
1450
|
+
* `result.byId` must be an object whose every key is a positive-integer id
|
|
1451
|
+
* spelled as a string and whose every value is a valid state;
|
|
1452
|
+
* `result.unavailable` an array of positive-integer ids.
|
|
1453
|
+
*/
|
|
1454
|
+
export function isValidEntityReactionsResult(p) {
|
|
1455
|
+
if (!isObject(p))
|
|
1456
|
+
return false;
|
|
1457
|
+
if (!isWireRequestIdShape(p.requestId))
|
|
1458
|
+
return false;
|
|
1459
|
+
if (p.error !== undefined && typeof p.error !== 'string')
|
|
1460
|
+
return false;
|
|
1461
|
+
if (p.result !== undefined) {
|
|
1462
|
+
const r = p.result;
|
|
1463
|
+
if (!isObject(r))
|
|
1464
|
+
return false;
|
|
1465
|
+
if (!isNonEmptyString(r.entityType))
|
|
1466
|
+
return false;
|
|
1467
|
+
if (!isObject(r.byId))
|
|
1468
|
+
return false;
|
|
1469
|
+
for (const [key, state] of Object.entries(r.byId)) {
|
|
1470
|
+
if (!/^[1-9][0-9]*$/.test(key))
|
|
1471
|
+
return false;
|
|
1472
|
+
if (!isValidEntityReactionState(state))
|
|
1473
|
+
return false;
|
|
1474
|
+
}
|
|
1475
|
+
if (!Array.isArray(r.unavailable))
|
|
1476
|
+
return false;
|
|
1477
|
+
for (const id of r.unavailable) {
|
|
1478
|
+
if (!isPositiveIntId(id))
|
|
1479
|
+
return false;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (p.result === undefined && p.error === undefined)
|
|
1483
|
+
return false;
|
|
1484
|
+
return true;
|
|
1485
|
+
}
|
|
1371
1486
|
/**
|
|
1372
1487
|
* Reply to a block-initiated `CREATE_POST_FROM_APP`. A well-formed reply carries
|
|
1373
1488
|
* EITHER a `result` (`{ postId: positive int, url: non-empty string, imageIds:
|
|
@@ -2041,6 +2156,60 @@ export function isValidImagePickerResult(p) {
|
|
|
2041
2156
|
}
|
|
2042
2157
|
return true;
|
|
2043
2158
|
}
|
|
2159
|
+
const MEDIA_UPLOAD_KINDS = new Set(['image', 'video', 'audio']);
|
|
2160
|
+
const isPositiveInteger = (v) => typeof v === 'number' && Number.isInteger(v) && v > 0;
|
|
2161
|
+
/**
|
|
2162
|
+
* One `BlockMediaUploadInfo`: a non-empty `url`, a known `kind`, a string
|
|
2163
|
+
* `mimeType`, a whole `sizeBytes` ≥ 0, and dimensions by kind. `width` and
|
|
2164
|
+
* `height` are positive whole numbers when present, REQUIRED for `image` and
|
|
2165
|
+
* `video` (the host always measures them) and ABSENT for `audio`. `durationMs`
|
|
2166
|
+
* is a finite number ≥ 0 when present.
|
|
2167
|
+
*/
|
|
2168
|
+
function isValidMediaUploadInfo(s) {
|
|
2169
|
+
if (!isObject(s))
|
|
2170
|
+
return false;
|
|
2171
|
+
if (!isNonEmptyString(s.url))
|
|
2172
|
+
return false;
|
|
2173
|
+
if (typeof s.kind !== 'string' || !MEDIA_UPLOAD_KINDS.has(s.kind))
|
|
2174
|
+
return false;
|
|
2175
|
+
if (typeof s.mimeType !== 'string')
|
|
2176
|
+
return false;
|
|
2177
|
+
if (!isFiniteNumber(s.sizeBytes) || !Number.isInteger(s.sizeBytes) || s.sizeBytes < 0)
|
|
2178
|
+
return false;
|
|
2179
|
+
if (s.kind === 'audio') {
|
|
2180
|
+
if (s.width !== undefined || s.height !== undefined)
|
|
2181
|
+
return false;
|
|
2182
|
+
}
|
|
2183
|
+
else if (!isPositiveInteger(s.width) || !isPositiveInteger(s.height)) {
|
|
2184
|
+
return false;
|
|
2185
|
+
}
|
|
2186
|
+
if (s.durationMs !== undefined && (!isFiniteNumber(s.durationMs) || s.durationMs < 0))
|
|
2187
|
+
return false;
|
|
2188
|
+
return true;
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Reply to `OPEN_MEDIA_UPLOAD`: `{ requestId, selected }`, `{ requestId, error }`
|
|
2192
|
+
* or a bare `{ requestId }` (dismissed / declined).
|
|
2193
|
+
*
|
|
2194
|
+
* `error` must be a string when present, and a reply carrying one is accepted
|
|
2195
|
+
* without looking further: `useMediaUpload` throws on a PRESENT `error` before
|
|
2196
|
+
* it reads `selected`. 🔴 Accepting it is load-bearing: dropping
|
|
2197
|
+
* `{ requestId, error }` would leave the hook waiting out its whole
|
|
2198
|
+
* human-interaction timeout on a refusal the host already sent.
|
|
2199
|
+
*/
|
|
2200
|
+
export function isValidMediaUploadResult(p) {
|
|
2201
|
+
if (!isObject(p))
|
|
2202
|
+
return false;
|
|
2203
|
+
if (!isWireRequestIdShape(p.requestId))
|
|
2204
|
+
return false;
|
|
2205
|
+
if (p.error !== undefined && typeof p.error !== 'string')
|
|
2206
|
+
return false;
|
|
2207
|
+
if (p.error !== undefined)
|
|
2208
|
+
return true; // hook throws before reading `selected`
|
|
2209
|
+
if (p.selected !== undefined && !isValidMediaUploadInfo(p.selected))
|
|
2210
|
+
return false;
|
|
2211
|
+
return true;
|
|
2212
|
+
}
|
|
2044
2213
|
/**
|
|
2045
2214
|
* Reply to `SET_USER_CHECKPOINT`. `ok` is required on the success path; the
|
|
2046
2215
|
* hook throws on `!ok || error !== undefined` — presence, not truthiness, so an
|
|
@@ -2167,6 +2336,10 @@ export function payloadValidatorFor(type) {
|
|
|
2167
2336
|
return isValidImagesResult;
|
|
2168
2337
|
case 'COLLECTION_FOLLOW_RESULT':
|
|
2169
2338
|
return isValidCollectionFollowResult;
|
|
2339
|
+
case 'ENTITY_REACTION_RESULT':
|
|
2340
|
+
return isValidEntityReactionResult;
|
|
2341
|
+
case 'ENTITY_REACTIONS_RESULT':
|
|
2342
|
+
return isValidEntityReactionsResult;
|
|
2170
2343
|
// 🔴 PRESENCE of this entry IS compiler-enforced (the `never` bind in
|
|
2171
2344
|
// `default:`); WHICH validator it names is not. This reply settles a PUBLIC
|
|
2172
2345
|
// POST, so the identity is pinned by test as well — see the mapping pin in
|
|
@@ -2223,6 +2396,8 @@ export function payloadValidatorFor(type) {
|
|
|
2223
2396
|
return isValidResourcePickerResult;
|
|
2224
2397
|
case 'IMAGE_PICKER_RESULT':
|
|
2225
2398
|
return isValidImagePickerResult;
|
|
2399
|
+
case 'MEDIA_UPLOAD_RESULT':
|
|
2400
|
+
return isValidMediaUploadResult;
|
|
2226
2401
|
case 'USER_CHECKPOINT_SET':
|
|
2227
2402
|
return isValidUserCheckpointSetResult;
|
|
2228
2403
|
case 'SUSPEND':
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/* AUTOGENERATED from package.json by scripts/gen-version.mjs. DO NOT EDIT. */
|
|
2
2
|
/** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
|
|
3
|
-
export const VERSION = "0.
|
|
3
|
+
export const VERSION = "0.76.0";
|
|
4
4
|
//# sourceMappingURL=version.generated.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@civitai/blocks-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.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",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"node": ">=20"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@civitai/components": "^0.
|
|
40
|
+
"@civitai/components": "^0.13.0",
|
|
41
41
|
"@civitai/theme": "^0.5.2"
|
|
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.",
|
|
@@ -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.75.0"
|
|
62
62
|
},
|
|
63
63
|
"publishConfig": {
|
|
64
64
|
"access": "public"
|