@civitai/blocks-react 0.75.0 → 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 +129 -0
- package/dist/hooks/useMediaUpload.d.ts +97 -0
- package/dist/hooks/useMediaUpload.js +108 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/internal/liveHost.d.ts +9 -1
- package/dist/internal/liveHost.js +27 -1
- package/dist/internal/mediaUploadBytes.d.ts +169 -0
- package/dist/internal/mediaUploadBytes.js +268 -0
- package/dist/internal/mockHost.d.ts +33 -1
- package/dist/internal/mockHost.js +69 -2
- 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 +1 -0
- package/dist/transport/requestTimeouts.js +1 -0
- package/dist/transport/validate.d.ts +11 -0
- package/dist/transport/validate.js +56 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +2 -2
|
@@ -27,7 +27,10 @@ export declare const MOCK_WORKFLOW_BOUNDS: {
|
|
|
27
27
|
readonly maxInputDepth: 32;
|
|
28
28
|
/** How deep a `repeat` template may nest another `repeat`. */
|
|
29
29
|
readonly maxRepeatNesting: 4;
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Serialized bytes of every step `input` plus `arguments`, summed. The host's
|
|
32
|
+
* `WORKFLOW_INPUT_BYTES_MAX`, which is its pass-through cap by definition.
|
|
33
|
+
*/
|
|
31
34
|
readonly maxInputBytes: 262144;
|
|
32
35
|
/** Characters of a step `$type`, and of a step `name` on the wire. */
|
|
33
36
|
readonly maxTypeChars: 64;
|
|
@@ -95,6 +98,22 @@ export declare function mockUnpricedWorkflowRefusal(body: {
|
|
|
95
98
|
* not already refuse it. Estimate only — the submit of the same body proceeds.
|
|
96
99
|
*/
|
|
97
100
|
export declare function mockUnquotedWorkflowEstimateRefusal(failure: MockWorkflowQuoteFailure): MockWorkflowRefusal;
|
|
101
|
+
type WellFormedWorkflowBody = {
|
|
102
|
+
steps: Array<{
|
|
103
|
+
name?: string;
|
|
104
|
+
$type: string;
|
|
105
|
+
input: Record<string, unknown>;
|
|
106
|
+
suppressOutput?: boolean;
|
|
107
|
+
}>;
|
|
108
|
+
arguments?: Record<string, unknown>;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* The host's wire schema, mirrored: exact keys, types and sizes — everything
|
|
112
|
+
* but the summed byte cap. Exported so the step-input gate
|
|
113
|
+
* (`mockHostStepInput.ts`) runs its rules only on a body the schema's shape
|
|
114
|
+
* accepts, as zod runs a refinement only after the shape parses.
|
|
115
|
+
*/
|
|
116
|
+
export declare function isWellFormedWorkflowShape(body: unknown): body is WellFormedWorkflowBody;
|
|
98
117
|
/**
|
|
99
118
|
* The refusal for a `kind: 'workflow'` body, or `null` for one the mock will
|
|
100
119
|
* price and run. In the host's order:
|
|
@@ -114,4 +133,5 @@ export declare function mockUnquotedWorkflowEstimateRefusal(failure: MockWorkflo
|
|
|
114
133
|
* `workflow_step_denied`.
|
|
115
134
|
*/
|
|
116
135
|
export declare function mockWorkflowBodyRefusal(body: unknown): MockWorkflowRefusal | null;
|
|
136
|
+
export {};
|
|
117
137
|
//# sourceMappingURL=mockWorkflowGraph.d.ts.map
|
|
@@ -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,7 @@ 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';
|
|
25
26
|
export { type MockReactionSeed } from './internal/mockHostReactions.js';
|
|
26
27
|
export { fakeResourceSource, type FakeResourceCanned, type FakeResourceSource, } from './internal/fakeResourceSource.js';
|
|
27
28
|
/**
|
|
@@ -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
|
|
@@ -605,6 +605,17 @@ export declare function isValidResourcePickerResult(p: unknown): boolean;
|
|
|
605
605
|
* out its whole human-interaction timeout on a refusal the host already sent.
|
|
606
606
|
*/
|
|
607
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;
|
|
608
619
|
/**
|
|
609
620
|
* Reply to `SET_USER_CHECKPOINT`. `ok` is required on the success path; the
|
|
610
621
|
* hook throws on `!ok || error !== undefined` — presence, not truthiness, so an
|
|
@@ -2156,6 +2156,60 @@ export function isValidImagePickerResult(p) {
|
|
|
2156
2156
|
}
|
|
2157
2157
|
return true;
|
|
2158
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
|
+
}
|
|
2159
2213
|
/**
|
|
2160
2214
|
* Reply to `SET_USER_CHECKPOINT`. `ok` is required on the success path; the
|
|
2161
2215
|
* hook throws on `!ok || error !== undefined` — presence, not truthiness, so an
|
|
@@ -2342,6 +2396,8 @@ export function payloadValidatorFor(type) {
|
|
|
2342
2396
|
return isValidResourcePickerResult;
|
|
2343
2397
|
case 'IMAGE_PICKER_RESULT':
|
|
2344
2398
|
return isValidImagePickerResult;
|
|
2399
|
+
case 'MEDIA_UPLOAD_RESULT':
|
|
2400
|
+
return isValidMediaUploadResult;
|
|
2345
2401
|
case 'USER_CHECKPOINT_SET':
|
|
2346
2402
|
return isValidUserCheckpointSetResult;
|
|
2347
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",
|
|
@@ -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"
|