@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.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * `OPEN_MEDIA_UPLOAD`: the strings and limits both sides of the bridge agree
3
+ * on, the content sniffers, and the mock host's copy of the host's admission
4
+ * rules for BYTES mode.
5
+ *
6
+ * The host owns the real rules. This copy exists so `createMockHost` (and so
7
+ * `dev:harness` and every block test) accepts and refuses the same buffers
8
+ * production does, and so `useMediaUpload` can refuse locally what the host
9
+ * would refuse anyway. Keep it exactly as narrow as the contract: a mock that
10
+ * accepts a GIF as an image teaches a block that GIFs upload.
11
+ *
12
+ * πŸ”΄ The host spells its OWN copy of every constant and string here, and the
13
+ * two MUST match. Nothing in this repository can read the host's values, so a
14
+ * drift is invisible here until a block hits a refusal the SDK did not
15
+ * predict. Kept local to this package (not exported from `@civitai/app-sdk`).
16
+ *
17
+ * WHAT THIS CANNOT MODEL: the host decodes the media before it uploads.
18
+ * Its decode-time refusals (`could not read this video file`,
19
+ * `could not read this audio file`, `this file has no video track`, an image
20
+ * too small to resize to at least 64 px, and upload failures) need a real
21
+ * decoder and network, so the mock never produces them.
22
+ */
23
+ import type { BlockMediaKind, BlockMediaUploadInfo } from '@civitai/app-sdk/blocks';
24
+ /** The cap on one VIDEO or AUDIO upload: 64 MiB of raw bytes (the consumer-blob upload cap). */
25
+ export declare const MEDIA_UPLOAD_MAX_BYTES: number;
26
+ /**
27
+ * The cap on one IMAGE upload: 24 MiB. Lower than {@link MEDIA_UPLOAD_MAX_BYTES}
28
+ * because the host decodes an image in full before it resizes it.
29
+ */
30
+ export declare const MEDIA_UPLOAD_IMAGE_MAX_BYTES: number;
31
+ /** The bytes-mode rolling window: per this many milliseconds, per mounted page host… */
32
+ export declare const MEDIA_UPLOAD_WINDOW_MS = 60000;
33
+ /** …at most this many admitted bytes uploads… */
34
+ export declare const MEDIA_UPLOAD_MAX_PER_WINDOW = 8;
35
+ /** …and this many admitted bytes. */
36
+ export declare const MEDIA_UPLOAD_MAX_BYTES_PER_WINDOW: number;
37
+ /** `bytes` is not a non-empty `ArrayBuffer`, or `mimeType` is present but not a string. */
38
+ export declare const MEDIA_UPLOAD_INVALID_ERROR = "invalid media-upload request";
39
+ export declare const MEDIA_UPLOAD_KIND_ERROR = "kind must be 'image', 'video' or 'audio'";
40
+ export declare const MEDIA_UPLOAD_PURPOSE_ERROR = "purpose must be 'generationSource'";
41
+ /** Over {@link MEDIA_UPLOAD_MAX_BYTES} (video, audio). */
42
+ export declare const MEDIA_UPLOAD_TOO_LARGE_ERROR = "file exceeds the 64 MB upload limit";
43
+ /** Over {@link MEDIA_UPLOAD_IMAGE_MAX_BYTES} (image). */
44
+ export declare const MEDIA_UPLOAD_IMAGE_TOO_LARGE_ERROR = "file exceeds the 24 MB image upload limit";
45
+ /** Bytes mode only: the host holds no block token. Checked before the cap and the window. */
46
+ export declare const MEDIA_UPLOAD_NO_TOKEN_ERROR = "no block token";
47
+ /** The sniffed content is not one of the requested kind's types. */
48
+ export declare const MEDIA_UPLOAD_TYPE_ERROR: Readonly<Record<BlockMediaKind, string>>;
49
+ export declare const MEDIA_UPLOAD_MIME_MISMATCH_ERROR = "mimeType does not match the file's content";
50
+ /** The bytes-mode window is full. Retryable. */
51
+ export declare const MEDIA_UPLOAD_BUSY_ERROR = "busy";
52
+ /** The kinds the bridge knows. */
53
+ export declare const MEDIA_UPLOAD_KINDS: readonly BlockMediaKind[];
54
+ export declare function isMediaKind(v: unknown): v is BlockMediaKind;
55
+ /** The per-file cap for a kind, and the refusal past it. */
56
+ export declare function mediaUploadCap(kind: BlockMediaKind): {
57
+ maxBytes: number;
58
+ error: string;
59
+ };
60
+ /** One admitted bytes upload in the window: when, and how many bytes. */
61
+ export type MediaUploadWindowEntry = {
62
+ at: number;
63
+ size: number;
64
+ };
65
+ /**
66
+ * The content type of `bytes` WITHIN the requested kind, by magic bytes, or
67
+ * `null` when the content is none of that kind's types (a PNG sent as `audio`
68
+ * is `null`: a type refusal). WebM is `video/webm` under `video` and
69
+ * `audio/webm` under `audio`.
70
+ */
71
+ export declare function sniffMediaBytes(kind: BlockMediaKind, u8: Uint8Array): string | null;
72
+ /** A declared MIME type as the host compares it: lowercased, `;params` cut, `audio/x-wav` read as `audio/wav`. */
73
+ export declare function normaliseMediaMimeType(mimeType: string): string;
74
+ /**
75
+ * The host's decision for one `OPEN_MEDIA_UPLOAD`, in the host's order, from
76
+ * step 4 of the contract on (the caller has already dropped an unroutable
77
+ * `requestId`; review mode and readiness do not exist in the mock):
78
+ * 1. `kind` not known β†’ kind error; `purpose` not `'generationSource'` β†’
79
+ * purpose error;
80
+ * 2. no `bytes` (absent or `null`) β†’ PICKER mode (not windowed: a human
81
+ * picks);
82
+ * 3. `bytes` not a non-empty `ArrayBuffer`, or a non-string `mimeType` β†’
83
+ * invalid;
84
+ * 4. no block token (`opts.noToken`) β†’ `no block token`, before the cap
85
+ * and the window;
86
+ * 5. over the kind's cap ({@link mediaUploadCap}: 24 MiB for an image,
87
+ * 64 MiB for video and audio) β†’ too large, before the window, so an
88
+ * over-cap file is never `busy` (a retry could never succeed);
89
+ * 6. the window, NOT recording, so a full window refuses `busy` before any
90
+ * sniffing;
91
+ * 7. the sniffed content in the kind's set β†’ else the kind's type error;
92
+ * 8. a present `mimeType` (normalised) equal to the sniffed type β†’ else
93
+ * mismatch;
94
+ * 9. record the upload in the window. Only an upload that will run counts.
95
+ * Returns the pruned window to store back.
96
+ */
97
+ export declare function admitMediaUpload(raw: {
98
+ kind?: unknown;
99
+ purpose?: unknown;
100
+ bytes?: unknown;
101
+ mimeType?: unknown;
102
+ }, recent: readonly MediaUploadWindowEntry[], now: number, opts?: {
103
+ noToken?: boolean;
104
+ }): {
105
+ result: {
106
+ ok: true;
107
+ mode: 'picker';
108
+ kind: BlockMediaKind;
109
+ } | {
110
+ ok: true;
111
+ mode: 'bytes';
112
+ kind: BlockMediaKind;
113
+ bytes: ArrayBuffer;
114
+ mimeType: string;
115
+ } | {
116
+ ok: false;
117
+ error: string;
118
+ };
119
+ recent: MediaUploadWindowEntry[];
120
+ };
121
+ /**
122
+ * The mock host's canned uploads, one per kind. A kind left out answers
123
+ * {@link DEFAULT_MOCK_MEDIA_UPLOADS}; `null` for a kind = the viewer closes that
124
+ * picker (bytes mode, which has no picker, then answers the default).
125
+ */
126
+ export type MockMediaUploads = Partial<Record<BlockMediaKind, BlockMediaUploadInfo | null>>;
127
+ /**
128
+ * What the mock host answers when no canned upload is configured for a kind.
129
+ * Every field differs between kinds, so a block that mixes two up shows it.
130
+ * The URLs are deliberately unresolvable: the mock uploads nothing.
131
+ */
132
+ export declare const DEFAULT_MOCK_MEDIA_UPLOADS: Readonly<Record<BlockMediaKind, BlockMediaUploadInfo>>;
133
+ /**
134
+ * The mock host's whole answer to one routable `OPEN_MEDIA_UPLOAD`, on a page
135
+ * surface: {@link admitMediaUpload}, then
136
+ * - picker mode: the canned upload for the kind, or a bare reply (dismissed)
137
+ * when that kind is canned `null`;
138
+ * - bytes mode: the canned upload for the kind (the default when it is
139
+ * `null`: there is no viewer to dismiss), carrying what the host would
140
+ * report for THESE bytes: the sniffed `mimeType` and the buffer's size for
141
+ * video and audio; `image/jpeg` and the canned size for an image, which the
142
+ * host re-encodes;
143
+ * - `forcedError`, when set, replaces a success: the stand-in for the
144
+ * decode and upload failures the mock cannot produce. A bytes upload it
145
+ * fails has still been admitted, so it still counts against the window, as
146
+ * on the host. ONE value is placed where the host checks it instead:
147
+ * `no block token` refuses a bytes upload right after the buffer check,
148
+ * before the cap and the window, and is not counted.
149
+ */
150
+ export declare function mockMediaUploadReply(args: {
151
+ payload: {
152
+ kind?: unknown;
153
+ purpose?: unknown;
154
+ bytes?: unknown;
155
+ mimeType?: unknown;
156
+ };
157
+ canned: MockMediaUploads;
158
+ forcedError: string | undefined;
159
+ recent: readonly MediaUploadWindowEntry[];
160
+ now: number;
161
+ }): {
162
+ reply: {
163
+ selected: BlockMediaUploadInfo;
164
+ } | {
165
+ error: string;
166
+ } | Record<string, never>;
167
+ recent: MediaUploadWindowEntry[];
168
+ };
169
+ //# sourceMappingURL=mediaUploadBytes.d.ts.map
@@ -0,0 +1,268 @@
1
+ /**
2
+ * `OPEN_MEDIA_UPLOAD`: the strings and limits both sides of the bridge agree
3
+ * on, the content sniffers, and the mock host's copy of the host's admission
4
+ * rules for BYTES mode.
5
+ *
6
+ * The host owns the real rules. This copy exists so `createMockHost` (and so
7
+ * `dev:harness` and every block test) accepts and refuses the same buffers
8
+ * production does, and so `useMediaUpload` can refuse locally what the host
9
+ * would refuse anyway. Keep it exactly as narrow as the contract: a mock that
10
+ * accepts a GIF as an image teaches a block that GIFs upload.
11
+ *
12
+ * πŸ”΄ The host spells its OWN copy of every constant and string here, and the
13
+ * two MUST match. Nothing in this repository can read the host's values, so a
14
+ * drift is invisible here until a block hits a refusal the SDK did not
15
+ * predict. Kept local to this package (not exported from `@civitai/app-sdk`).
16
+ *
17
+ * WHAT THIS CANNOT MODEL: the host decodes the media before it uploads.
18
+ * Its decode-time refusals (`could not read this video file`,
19
+ * `could not read this audio file`, `this file has no video track`, an image
20
+ * too small to resize to at least 64 px, and upload failures) need a real
21
+ * decoder and network, so the mock never produces them.
22
+ */
23
+ import { sniffImageBytes } from './saveBytes.js';
24
+ /** The cap on one VIDEO or AUDIO upload: 64 MiB of raw bytes (the consumer-blob upload cap). */
25
+ export const MEDIA_UPLOAD_MAX_BYTES = 64 * 1024 * 1024;
26
+ /**
27
+ * The cap on one IMAGE upload: 24 MiB. Lower than {@link MEDIA_UPLOAD_MAX_BYTES}
28
+ * because the host decodes an image in full before it resizes it.
29
+ */
30
+ export const MEDIA_UPLOAD_IMAGE_MAX_BYTES = 24 * 1024 * 1024;
31
+ /** The bytes-mode rolling window: per this many milliseconds, per mounted page host… */
32
+ export const MEDIA_UPLOAD_WINDOW_MS = 60_000;
33
+ /** …at most this many admitted bytes uploads… */
34
+ export const MEDIA_UPLOAD_MAX_PER_WINDOW = 8;
35
+ /** …and this many admitted bytes. */
36
+ export const MEDIA_UPLOAD_MAX_BYTES_PER_WINDOW = 2 * MEDIA_UPLOAD_MAX_BYTES;
37
+ /** `bytes` is not a non-empty `ArrayBuffer`, or `mimeType` is present but not a string. */
38
+ export const MEDIA_UPLOAD_INVALID_ERROR = 'invalid media-upload request';
39
+ export const MEDIA_UPLOAD_KIND_ERROR = "kind must be 'image', 'video' or 'audio'";
40
+ export const MEDIA_UPLOAD_PURPOSE_ERROR = "purpose must be 'generationSource'";
41
+ /** Over {@link MEDIA_UPLOAD_MAX_BYTES} (video, audio). */
42
+ export const MEDIA_UPLOAD_TOO_LARGE_ERROR = 'file exceeds the 64 MB upload limit';
43
+ /** Over {@link MEDIA_UPLOAD_IMAGE_MAX_BYTES} (image). */
44
+ export const MEDIA_UPLOAD_IMAGE_TOO_LARGE_ERROR = 'file exceeds the 24 MB image upload limit';
45
+ /** Bytes mode only: the host holds no block token. Checked before the cap and the window. */
46
+ export const MEDIA_UPLOAD_NO_TOKEN_ERROR = 'no block token';
47
+ /** The sniffed content is not one of the requested kind's types. */
48
+ export const MEDIA_UPLOAD_TYPE_ERROR = {
49
+ image: 'images must be PNG, JPEG or WebP',
50
+ video: 'videos must be MP4 or WebM',
51
+ audio: 'audio must be MP3, WAV or WebM',
52
+ };
53
+ export const MEDIA_UPLOAD_MIME_MISMATCH_ERROR = "mimeType does not match the file's content";
54
+ /** The bytes-mode window is full. Retryable. */
55
+ export const MEDIA_UPLOAD_BUSY_ERROR = 'busy';
56
+ /** The kinds the bridge knows. */
57
+ export const MEDIA_UPLOAD_KINDS = ['image', 'video', 'audio'];
58
+ export function isMediaKind(v) {
59
+ return typeof v === 'string' && MEDIA_UPLOAD_KINDS.includes(v);
60
+ }
61
+ /** The per-file cap for a kind, and the refusal past it. */
62
+ export function mediaUploadCap(kind) {
63
+ return kind === 'image'
64
+ ? { maxBytes: MEDIA_UPLOAD_IMAGE_MAX_BYTES, error: MEDIA_UPLOAD_IMAGE_TOO_LARGE_ERROR }
65
+ : { maxBytes: MEDIA_UPLOAD_MAX_BYTES, error: MEDIA_UPLOAD_TOO_LARGE_ERROR };
66
+ }
67
+ function asciiAt(u8, text, offset = 0) {
68
+ if (u8.length < offset + text.length)
69
+ return false;
70
+ for (let i = 0; i < text.length; i++) {
71
+ if (u8[offset + i] !== text.charCodeAt(i))
72
+ return false;
73
+ }
74
+ return true;
75
+ }
76
+ /** WebM: the EBML magic, and the DocType `webm` within the first 64 bytes (generic Matroska is refused). */
77
+ function isWebm(u8) {
78
+ if (!(u8[0] === 0x1a && u8[1] === 0x45 && u8[2] === 0xdf && u8[3] === 0xa3))
79
+ return false;
80
+ const head = u8.subarray(0, 64);
81
+ for (let i = 0; i + 4 <= head.length; i++) {
82
+ if (asciiAt(head, 'webm', i))
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+ /** MP4: `ftyp` at offset 4 of a buffer at least 12 bytes long. */
88
+ function isMp4(u8) {
89
+ return u8.length >= 12 && asciiAt(u8, 'ftyp', 4);
90
+ }
91
+ /** MP3: an ID3 tag, or an MPEG audio frame sync with a valid version and layer. */
92
+ function isMp3(u8) {
93
+ if (asciiAt(u8, 'ID3'))
94
+ return true;
95
+ if (u8.length < 2)
96
+ return false;
97
+ const b0 = u8[0];
98
+ const b1 = u8[1];
99
+ return b0 === 0xff && (b1 & 0xe0) === 0xe0 && ((b1 >> 1) & 0x03) !== 0 && ((b1 >> 3) & 0x03) !== 1;
100
+ }
101
+ /** WAV: `RIFF` at 0 and `WAVE` at 8. */
102
+ function isWav(u8) {
103
+ return asciiAt(u8, 'RIFF') && asciiAt(u8, 'WAVE', 8);
104
+ }
105
+ /**
106
+ * The content type of `bytes` WITHIN the requested kind, by magic bytes, or
107
+ * `null` when the content is none of that kind's types (a PNG sent as `audio`
108
+ * is `null`: a type refusal). WebM is `video/webm` under `video` and
109
+ * `audio/webm` under `audio`.
110
+ */
111
+ export function sniffMediaBytes(kind, u8) {
112
+ switch (kind) {
113
+ case 'image':
114
+ return sniffImageBytes(u8);
115
+ case 'video':
116
+ if (isMp4(u8))
117
+ return 'video/mp4';
118
+ if (isWebm(u8))
119
+ return 'video/webm';
120
+ return null;
121
+ case 'audio':
122
+ if (isMp3(u8))
123
+ return 'audio/mpeg';
124
+ if (isWav(u8))
125
+ return 'audio/wav';
126
+ if (isWebm(u8))
127
+ return 'audio/webm';
128
+ return null;
129
+ }
130
+ }
131
+ /** A declared MIME type as the host compares it: lowercased, `;params` cut, `audio/x-wav` read as `audio/wav`. */
132
+ export function normaliseMediaMimeType(mimeType) {
133
+ const base = mimeType.split(';')[0].trim().toLowerCase();
134
+ return base === 'audio/x-wav' ? 'audio/wav' : base;
135
+ }
136
+ function windowHasRoom(live, size) {
137
+ const total = live.reduce((sum, e) => sum + e.size, 0);
138
+ return live.length < MEDIA_UPLOAD_MAX_PER_WINDOW && total + size <= MEDIA_UPLOAD_MAX_BYTES_PER_WINDOW;
139
+ }
140
+ /**
141
+ * The host's decision for one `OPEN_MEDIA_UPLOAD`, in the host's order, from
142
+ * step 4 of the contract on (the caller has already dropped an unroutable
143
+ * `requestId`; review mode and readiness do not exist in the mock):
144
+ * 1. `kind` not known β†’ kind error; `purpose` not `'generationSource'` β†’
145
+ * purpose error;
146
+ * 2. no `bytes` (absent or `null`) β†’ PICKER mode (not windowed: a human
147
+ * picks);
148
+ * 3. `bytes` not a non-empty `ArrayBuffer`, or a non-string `mimeType` β†’
149
+ * invalid;
150
+ * 4. no block token (`opts.noToken`) β†’ `no block token`, before the cap
151
+ * and the window;
152
+ * 5. over the kind's cap ({@link mediaUploadCap}: 24 MiB for an image,
153
+ * 64 MiB for video and audio) β†’ too large, before the window, so an
154
+ * over-cap file is never `busy` (a retry could never succeed);
155
+ * 6. the window, NOT recording, so a full window refuses `busy` before any
156
+ * sniffing;
157
+ * 7. the sniffed content in the kind's set β†’ else the kind's type error;
158
+ * 8. a present `mimeType` (normalised) equal to the sniffed type β†’ else
159
+ * mismatch;
160
+ * 9. record the upload in the window. Only an upload that will run counts.
161
+ * Returns the pruned window to store back.
162
+ */
163
+ export function admitMediaUpload(raw, recent, now, opts = {}) {
164
+ const live = recent.filter((e) => now - e.at < MEDIA_UPLOAD_WINDOW_MS);
165
+ const refuse = (error) => ({ result: { ok: false, error }, recent: live });
166
+ if (!isMediaKind(raw.kind))
167
+ return refuse(MEDIA_UPLOAD_KIND_ERROR);
168
+ const kind = raw.kind;
169
+ if (raw.purpose !== 'generationSource')
170
+ return refuse(MEDIA_UPLOAD_PURPOSE_ERROR);
171
+ // `null` reads as absent, as `OPEN_IMAGE_UPLOAD`'s `bytes` does.
172
+ if (raw.bytes == null)
173
+ return { result: { ok: true, mode: 'picker', kind }, recent: live };
174
+ const bytes = raw.bytes;
175
+ if (!(bytes instanceof ArrayBuffer) || bytes.byteLength === 0)
176
+ return refuse(MEDIA_UPLOAD_INVALID_ERROR);
177
+ if (raw.mimeType !== undefined && typeof raw.mimeType !== 'string') {
178
+ return refuse(MEDIA_UPLOAD_INVALID_ERROR);
179
+ }
180
+ const size = bytes.byteLength;
181
+ if (opts.noToken)
182
+ return refuse(MEDIA_UPLOAD_NO_TOKEN_ERROR);
183
+ const cap = mediaUploadCap(kind);
184
+ if (size > cap.maxBytes)
185
+ return refuse(cap.error);
186
+ if (!windowHasRoom(live, size))
187
+ return refuse(MEDIA_UPLOAD_BUSY_ERROR);
188
+ const sniffed = sniffMediaBytes(kind, new Uint8Array(bytes));
189
+ if (!sniffed)
190
+ return refuse(MEDIA_UPLOAD_TYPE_ERROR[kind]);
191
+ if (typeof raw.mimeType === 'string' && normaliseMediaMimeType(raw.mimeType) !== sniffed) {
192
+ return refuse(MEDIA_UPLOAD_MIME_MISMATCH_ERROR);
193
+ }
194
+ return {
195
+ result: { ok: true, mode: 'bytes', kind, bytes, mimeType: sniffed },
196
+ recent: [...live, { at: now, size }],
197
+ };
198
+ }
199
+ /**
200
+ * What the mock host answers when no canned upload is configured for a kind.
201
+ * Every field differs between kinds, so a block that mixes two up shows it.
202
+ * The URLs are deliberately unresolvable: the mock uploads nothing.
203
+ */
204
+ export const DEFAULT_MOCK_MEDIA_UPLOADS = {
205
+ image: {
206
+ url: 'https://media.mock.invalid/generation-input/dev-image.jpeg',
207
+ kind: 'image',
208
+ mimeType: 'image/jpeg',
209
+ sizeBytes: 182_311,
210
+ width: 1024,
211
+ height: 768,
212
+ },
213
+ video: {
214
+ url: 'https://media.mock.invalid/generation-input/dev-video.mp4',
215
+ kind: 'video',
216
+ mimeType: 'video/mp4',
217
+ sizeBytes: 4_812_330,
218
+ width: 1280,
219
+ height: 720,
220
+ durationMs: 5_000,
221
+ },
222
+ audio: {
223
+ url: 'https://media.mock.invalid/generation-input/dev-audio.mp3',
224
+ kind: 'audio',
225
+ mimeType: 'audio/mpeg',
226
+ sizeBytes: 961_204,
227
+ durationMs: 30_000,
228
+ },
229
+ };
230
+ /**
231
+ * The mock host's whole answer to one routable `OPEN_MEDIA_UPLOAD`, on a page
232
+ * surface: {@link admitMediaUpload}, then
233
+ * - picker mode: the canned upload for the kind, or a bare reply (dismissed)
234
+ * when that kind is canned `null`;
235
+ * - bytes mode: the canned upload for the kind (the default when it is
236
+ * `null`: there is no viewer to dismiss), carrying what the host would
237
+ * report for THESE bytes: the sniffed `mimeType` and the buffer's size for
238
+ * video and audio; `image/jpeg` and the canned size for an image, which the
239
+ * host re-encodes;
240
+ * - `forcedError`, when set, replaces a success: the stand-in for the
241
+ * decode and upload failures the mock cannot produce. A bytes upload it
242
+ * fails has still been admitted, so it still counts against the window, as
243
+ * on the host. ONE value is placed where the host checks it instead:
244
+ * `no block token` refuses a bytes upload right after the buffer check,
245
+ * before the cap and the window, and is not counted.
246
+ */
247
+ export function mockMediaUploadReply(args) {
248
+ const admitted = admitMediaUpload(args.payload, args.recent, args.now, {
249
+ noToken: args.forcedError === MEDIA_UPLOAD_NO_TOKEN_ERROR,
250
+ });
251
+ const { result, recent } = admitted;
252
+ if (!result.ok)
253
+ return { reply: { error: result.error }, recent };
254
+ if (args.forcedError !== undefined)
255
+ return { reply: { error: args.forcedError }, recent };
256
+ // A kind left out of `canned` answers the default; one set to `null` dismisses.
257
+ const configured = args.canned[result.kind];
258
+ const canned = configured === undefined ? DEFAULT_MOCK_MEDIA_UPLOADS[result.kind] : configured;
259
+ if (result.mode === 'picker') {
260
+ return { reply: canned ? { selected: { ...canned, kind: result.kind } } : {}, recent };
261
+ }
262
+ const base = canned ?? DEFAULT_MOCK_MEDIA_UPLOADS[result.kind];
263
+ const selected = result.kind === 'image'
264
+ ? { ...base, kind: 'image', mimeType: 'image/jpeg' }
265
+ : { ...base, kind: result.kind, mimeType: result.mimeType, sizeBytes: result.bytes.byteLength };
266
+ return { reply: { selected }, recent };
267
+ }
268
+ //# sourceMappingURL=mediaUploadBytes.js.map
@@ -37,8 +37,10 @@
37
37
  * called). Only the postMessage bridge round-trips are exercised. Never import
38
38
  * this from production code.
39
39
  */
40
- import { type BlockContext, type BlockResourceInfo, type BlockResourcePickerType, type BlockUploadedImageInfo, type BlockGenerationSourceImageInfo, type BuzzAccountType, type BlockBuzzTransaction, type BlockBuzzAccount, type BlockDailyCompensationResource, type BlockViewer, type BlockWildcardPack, type BlockWildcardPackErrorCode, type AppWorkflow, type BlockModel3dOutput, type BlockPublishedModel, type BlockRunConfirmedStepHostError, type BlockGatedImage, type BlockPickedImageInfo, type BlockCollectionFollowErrorCode, type BlockCreatePostHostError, type BlockCreatePostResult, type BlockPrepareTrainingDatasetHostError, type BlockRunTrainingHostError, type BlockTrainingRejectionReason, type ColorDomain, type SharedStorageValue, type Theme, type ViewerInfo, type WorkflowBody } from '@civitai/app-sdk/blocks';
40
+ import { type BlockContext, type BlockResourceInfo, type BlockResourcePickerType, type BlockUploadedImageInfo, type BlockGenerationSourceImageInfo, type BuzzAccountType, type BlockBuzzTransaction, type BlockBuzzAccount, type BlockDailyCompensationResource, type BlockViewer, type BlockWildcardPack, type BlockWildcardPackErrorCode, type AppWorkflow, type BlockModel3dOutput, type BlockPublishedModel, type BlockRunConfirmedStepHostError, type BlockGatedImage, type BlockPickedImageInfo, type BlockCollectionFollowErrorCode, type BlockEntityReactionErrorCode, type BlockCreatePostHostError, type BlockCreatePostResult, type BlockPrepareTrainingDatasetHostError, type BlockRunTrainingHostError, type BlockTrainingRejectionReason, type ColorDomain, type SharedStorageValue, type Theme, type ViewerInfo, type WorkflowBody } from '@civitai/app-sdk/blocks';
41
+ import { type MockMediaUploads } from './mediaUploadBytes.js';
41
42
  import { type MockWorkflowQuoteFailure } from './mockWorkflowGraph.js';
43
+ import { type MockReactionSeed } from './mockHostReactions.js';
42
44
  /**
43
45
  * How submits behave. `'none'` = everything succeeds; `'all'` / `'insufficient'`
44
46
  * = every submit fails as if the viewer were OUT OF BUZZ, until an
@@ -945,6 +947,33 @@ export interface MockHostOptions {
945
947
  * handling is only exercised by `declined`, never by the timing.
946
948
  */
947
949
  collectionFollowError?: BlockCollectionFollowErrorCode | string;
950
+ /**
951
+ * The images `GET_ENTITY_REACTIONS` / `SET_ENTITY_REACTION` know, keyed by
952
+ * image id: counts and the viewer's existing reactions. An id that is not
953
+ * seeded starts with zero counts and no reactions. Writes persist for the
954
+ * life of the mock. Live-tunable via {@link MockHost.setScenario} (a new seed
955
+ * REPLACES the state).
956
+ *
957
+ * πŸ”΄ BOTH MESSAGES ARE SCOPE-GATED HERE, as on the real host: unless
958
+ * {@link MockHostOptions.declaredScopes} includes `social:react:self`, every
959
+ * call replies `needs-consent`. A `null` {@link MockHostOptions.viewer}
960
+ * replies `sign-in-required`. Any well-formed ticket is accepted β€” a real one
961
+ * is an HMAC this mock cannot verify β€” but an image WITHOUT one is refused,
962
+ * as production refuses it. See `./mockHostReactions.ts`.
963
+ */
964
+ reactionStates?: Record<number, MockReactionSeed>;
965
+ /**
966
+ * Force both reaction messages to reply with this `error` once the request
967
+ * would otherwise proceed β€” one of `REACTION_ERROR_CODES` (`rate-limited`,
968
+ * `entity-unavailable`, `review-mode`, `not-ready`…) or any string for a code
969
+ * a newer host might add. Absent β†’ the request succeeds. Live-tunable via
970
+ * {@link MockHost.setScenario}; pass `null` there to clear it.
971
+ *
972
+ * `review-mode` and `not-ready` are answered FIRST, ahead of the sign-in,
973
+ * payload and scope checks, because the real host's gate checks them first;
974
+ * every other code is answered only once those pass, as a server answer is.
975
+ */
976
+ reactionError?: BlockEntityReactionErrorCode | string | null;
948
977
  /**
949
978
  * The post reported on `CREATE_POST_FROM_APP` (what
950
979
  * `useCreatePostFromApp().createPost()` resolves with). Absent β†’
@@ -1192,6 +1221,37 @@ export interface MockHostOptions {
1192
1221
  mimeType: string;
1193
1222
  filename: string;
1194
1223
  }) => void;
1224
+ /**
1225
+ * What `OPEN_MEDIA_UPLOAD` (`useMediaUpload()`) answers, per kind. A kind
1226
+ * left out answers a built-in default for that kind; `null` makes the viewer
1227
+ * close that kind's picker (`open()` resolves `null`). A bytes `upload()`
1228
+ * answers the kind's entry (or the default) with the sniffed `mimeType` and
1229
+ * the buffer's `sizeBytes` for video and audio, and `image/jpeg` for an
1230
+ * image, as the host re-encodes it.
1231
+ *
1232
+ * The host's checks run in the host's order (`./mediaUploadBytes.ts`): kind,
1233
+ * purpose, then for bytes an invalid buffer, the cap (24 MB for an image,
1234
+ * 64 MB for video and audio), the bytes-mode
1235
+ * window (8 uploads or 128 MB per 60 s, per install) β†’ `busy`, the content
1236
+ * sniff per kind, and a declared `mimeType` that disagrees. A model-slot
1237
+ * `context` answers `unsupported on this host`. πŸ”΄ The mock cannot decode
1238
+ * media, so the host's decode-time refusals (an unreadable file, a video with
1239
+ * no video track, an image too small to resize) never happen here: force one
1240
+ * with {@link mediaUploadError}. Live-tunable via {@link MockHost.setScenario}.
1241
+ */
1242
+ cannedMediaUploads?: MockMediaUploads;
1243
+ /**
1244
+ * Force an `OPEN_MEDIA_UPLOAD` that passed the checks above to reply with
1245
+ * this `error`: a host-only refusal such as `this file has no video track`,
1246
+ * `media upload failed β€” please try again`, or
1247
+ * `this video was refused by the upload service`. A bytes upload it fails
1248
+ * still counts against the window, as on the host. `no block token` is the
1249
+ * exception: like the host, the mock refuses a bytes upload with it right
1250
+ * after the buffer check, before the cap and the window, and does not count
1251
+ * it. Absent β†’ it succeeds.
1252
+ * Live-tunable via {@link MockHost.setScenario}; `undefined` clears it.
1253
+ */
1254
+ mediaUploadError?: string;
1195
1255
  /**
1196
1256
  * Image ids the mock treats as already issued and postable when the host is
1197
1257
  * created, as production treats an image this app stamped for the viewer in
@@ -1370,7 +1430,7 @@ export interface MockHostOptions {
1370
1430
  * {@link MockHost.setTheme}, which pushes a real `THEME_CHANGE` β€” the mock of
1371
1431
  * the viewer toggling dark mode with the block already mounted.)
1372
1432
  */
1373
- export type MockHostScenarioPatch = Pick<MockHostOptions, 'consentGrantable' | 'failMode' | 'cost' | 'pollsUntilDone' | 'cannedPicks' | 'cannedMultiPicks' | 'cannedImagePicks' | 'cannedImageUpload' | 'cannedGenerationSourceUpload' | 'cannedImageScan' | 'generation' | 'buzz' | 'storage' | 'shared' | 'buzzBalanceError' | 'viewerResult' | 'viewerError' | 'buzzTransactions' | 'buzzAccounts' | 'dailyCompensation' | 'buzzReadError' | 'wildcardPack' | 'wildcardPackError' | 'collectionFollowError' | 'createPostResult' | 'createPostError' | 'trainingDatasetRejected' | 'trainingDatasetError' | 'trainingQuoteTotal' | 'runTrainingError' | 'runTrainingCapRefusal' | 'runConfirmedStepError' | 'runConfirmedStepCapRefusal' | 'appWorkflows' | 'appWorkflowsError' | 'publishImageIds' | 'publishError' | 'gatedImages' | 'gatedImagesError' | 'disallowedAccountTypes' | 'saveImageError' | 'uploadImageBytesResult' | 'uploadImageBytesError'>;
1433
+ export type MockHostScenarioPatch = Pick<MockHostOptions, 'consentGrantable' | 'failMode' | 'cost' | 'pollsUntilDone' | 'cannedPicks' | 'cannedMultiPicks' | 'cannedImagePicks' | 'cannedImageUpload' | 'cannedGenerationSourceUpload' | 'cannedImageScan' | 'generation' | 'buzz' | 'storage' | 'shared' | 'buzzBalanceError' | 'viewerResult' | 'viewerError' | 'buzzTransactions' | 'buzzAccounts' | 'dailyCompensation' | 'buzzReadError' | 'wildcardPack' | 'wildcardPackError' | 'collectionFollowError' | 'reactionStates' | 'reactionError' | 'createPostResult' | 'createPostError' | 'trainingDatasetRejected' | 'trainingDatasetError' | 'trainingQuoteTotal' | 'runTrainingError' | 'runTrainingCapRefusal' | 'runConfirmedStepError' | 'runConfirmedStepCapRefusal' | 'appWorkflows' | 'appWorkflowsError' | 'publishImageIds' | 'publishError' | 'gatedImages' | 'gatedImagesError' | 'disallowedAccountTypes' | 'saveImageError' | 'uploadImageBytesResult' | 'uploadImageBytesError' | 'cannedMediaUploads' | 'mediaUploadError'>;
1374
1434
  /** Runtime Buzz-balance handle exposed on {@link MockHost.buzz}. */
1375
1435
  export interface MockBuzzHandle {
1376
1436
  /** Current simulated balance, or `undefined` when balance isn't simulated. */