@civitai/blocks-react 0.75.0 → 0.76.1

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.
@@ -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
- /** Serialized bytes of every step `input` plus `arguments`, summed. */
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
- /** Serialized bytes of every step `input` plus `arguments`, summed. */
31
- maxInputBytes: 262_144,
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
- /** The host's wire schema, mirrored: exact keys, types and sizes. No `errorCode`. */
339
- function isWellFormedBody(body) {
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
@@ -21,8 +21,8 @@ import { type SniffedImageType } from './saveBytes.js';
21
21
  * app-sdk peer.
22
22
  */
23
23
  export declare const UPLOAD_BYTES_MAX_BYTES: number;
24
- /** The host's rolling window: at most this many admitted uploads… */
25
- export declare const UPLOAD_BYTES_MAX_PER_WINDOW = 3;
24
+ /** The host's rolling window: at most this many admitted uploads (8 since civitai/civitai#5888)… */
25
+ export declare const UPLOAD_BYTES_MAX_PER_WINDOW = 8;
26
26
  /** …and this many admitted bytes… */
27
27
  export declare const UPLOAD_BYTES_MAX_BYTES_PER_WINDOW: number;
28
28
  /** …per this many milliseconds, per mounted page host. */
@@ -21,8 +21,8 @@ import { forceSaveBytesExtension, sanitizeSaveBytesFilename, sniffImageBytes, }
21
21
  * app-sdk peer.
22
22
  */
23
23
  export const UPLOAD_BYTES_MAX_BYTES = 40 * 1024 * 1024;
24
- /** The host's rolling window: at most this many admitted uploads… */
25
- export const UPLOAD_BYTES_MAX_PER_WINDOW = 3;
24
+ /** The host's rolling window: at most this many admitted uploads (8 since civitai/civitai#5888)… */
25
+ export const UPLOAD_BYTES_MAX_PER_WINDOW = 8;
26
26
  /** …and this many admitted bytes… */
27
27
  export const UPLOAD_BYTES_MAX_BYTES_PER_WINDOW = 2 * UPLOAD_BYTES_MAX_BYTES;
28
28
  /** …per this many milliseconds, per mounted page host. */
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,3 +1,3 @@
1
1
  /** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
2
- export declare const VERSION = "0.75.0";
2
+ export declare const VERSION = "0.76.1";
3
3
  //# sourceMappingURL=version.generated.d.ts.map
@@ -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.75.0";
3
+ export const VERSION = "0.76.1";
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.75.0",
3
+ "version": "0.76.1",
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.74.0"
61
+ "@civitai/app-sdk": "^0.75.0"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public"