@civitai/blocks-react 0.65.2 → 0.67.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,89 @@
1
+ /**
2
+ * The `bytes` variant of `OPEN_IMAGE_UPLOAD`: the strings both sides of the
3
+ * bridge agree on, and the mock host's copy of the host's admission rules.
4
+ *
5
+ * The contract is civitai/civitai#5639 (`imageUploadBytes.ts` in the host). The
6
+ * 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. Keep it exactly as narrow as the contract: a mock that
9
+ * accepts a GIF teaches a block that GIFs upload.
10
+ */
11
+ import { type SniffedImageType } from './saveBytes.js';
12
+ /**
13
+ * The cap on one `bytes` upload: 40 MiB of raw bytes (`ArrayBuffer.byteLength`).
14
+ *
15
+ * 🔴 The host spells its OWN copy of this number (civitai/civitai
16
+ * `BLOCK_IMAGE_MAX_BYTES`, the image pipeline's server cap), and the two MUST
17
+ * match. Nothing in this repository can read the host's value, so a drift is
18
+ * invisible here until a block hits the host's refusal on a buffer this hook let
19
+ * through, or the hook refuses one the host would have accepted. Kept local to
20
+ * this package (not exported from `@civitai/app-sdk`) so the hook needs no newer
21
+ * app-sdk peer.
22
+ */
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;
26
+ /** …and this many admitted bytes… */
27
+ export declare const UPLOAD_BYTES_MAX_BYTES_PER_WINDOW: number;
28
+ /** …per this many milliseconds, per mounted page host. */
29
+ export declare const UPLOAD_BYTES_WINDOW_MS = 60000;
30
+ /** Not a non-empty `ArrayBuffer`, or `purpose: 'generationSource'` with `bytes`. */
31
+ export declare const UPLOAD_BYTES_INVALID_ERROR = "invalid image-upload request";
32
+ /** Over {@link UPLOAD_BYTES_MAX_BYTES}. Also the hook's own pre-send refusal. */
33
+ export declare const UPLOAD_BYTES_TOO_LARGE_ERROR = "file exceeds the maximum upload size";
34
+ /** Not PNG / WebP / JPEG by magic bytes. */
35
+ export declare const UPLOAD_BYTES_TYPE_NOT_ALLOWED_ERROR = "file type is not allowed";
36
+ /** The host's rolling window is full. Retryable. */
37
+ export declare const UPLOAD_BYTES_BUSY_ERROR = "busy";
38
+ /** The host holds no block token. */
39
+ export declare const UPLOAD_BYTES_NO_TOKEN_ERROR = "no block token";
40
+ /** One admitted upload in the window: when, and how many bytes. */
41
+ export type UploadBytesWindowEntry = {
42
+ at: number;
43
+ size: number;
44
+ };
45
+ /**
46
+ * Which `OPEN_IMAGE_UPLOAD` variant a raw payload is, by the host's predicate
47
+ * (`resolveImageUploadBytes`). `none`: no `bytes` (null or absent), so the
48
+ * picker paths run unchanged. `invalid`: `bytes` is not an `ArrayBuffer` (a
49
+ * typed-array view is refused, not unwrapped), is empty, or comes with
50
+ * `purpose: 'generationSource'`.
51
+ */
52
+ export declare function resolveUploadBytesRequest(raw: {
53
+ bytes?: unknown;
54
+ purpose?: unknown;
55
+ filename?: unknown;
56
+ }): {
57
+ kind: 'none';
58
+ } | {
59
+ kind: 'invalid';
60
+ } | {
61
+ kind: 'bytes';
62
+ bytes: ArrayBuffer;
63
+ filename?: string;
64
+ };
65
+ /**
66
+ * The host's admission decision (`processUploadBytes`), in its order:
67
+ * 1. the per-file cap, FIRST, so an over-cap file is always too large and
68
+ * never `busy` (a retry could never succeed);
69
+ * 2. the window, NOT recording, so a full window refuses `busy` before any
70
+ * sniffing;
71
+ * 3. PNG / WebP / JPEG by magic bytes;
72
+ * 4. record the upload in the window. Only an upload that will run counts.
73
+ * Returns the pruned window to store back.
74
+ */
75
+ export declare function admitUploadBytes(req: {
76
+ bytes: ArrayBuffer;
77
+ filename?: string;
78
+ }, recent: readonly UploadBytesWindowEntry[], now: number): {
79
+ result: {
80
+ ok: true;
81
+ type: SniffedImageType;
82
+ filename: string;
83
+ } | {
84
+ ok: false;
85
+ error: string;
86
+ };
87
+ recent: UploadBytesWindowEntry[];
88
+ };
89
+ //# sourceMappingURL=uploadBytes.d.ts.map
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The `bytes` variant of `OPEN_IMAGE_UPLOAD`: the strings both sides of the
3
+ * bridge agree on, and the mock host's copy of the host's admission rules.
4
+ *
5
+ * The contract is civitai/civitai#5639 (`imageUploadBytes.ts` in the host). The
6
+ * 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. Keep it exactly as narrow as the contract: a mock that
9
+ * accepts a GIF teaches a block that GIFs upload.
10
+ */
11
+ import { forceSaveBytesExtension, sanitizeSaveBytesFilename, sniffImageBytes, } from './saveBytes.js';
12
+ /**
13
+ * The cap on one `bytes` upload: 40 MiB of raw bytes (`ArrayBuffer.byteLength`).
14
+ *
15
+ * 🔴 The host spells its OWN copy of this number (civitai/civitai
16
+ * `BLOCK_IMAGE_MAX_BYTES`, the image pipeline's server cap), and the two MUST
17
+ * match. Nothing in this repository can read the host's value, so a drift is
18
+ * invisible here until a block hits the host's refusal on a buffer this hook let
19
+ * through, or the hook refuses one the host would have accepted. Kept local to
20
+ * this package (not exported from `@civitai/app-sdk`) so the hook needs no newer
21
+ * app-sdk peer.
22
+ */
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;
26
+ /** …and this many admitted bytes… */
27
+ export const UPLOAD_BYTES_MAX_BYTES_PER_WINDOW = 2 * UPLOAD_BYTES_MAX_BYTES;
28
+ /** …per this many milliseconds, per mounted page host. */
29
+ export const UPLOAD_BYTES_WINDOW_MS = 60_000;
30
+ /** Not a non-empty `ArrayBuffer`, or `purpose: 'generationSource'` with `bytes`. */
31
+ export const UPLOAD_BYTES_INVALID_ERROR = 'invalid image-upload request';
32
+ /** Over {@link UPLOAD_BYTES_MAX_BYTES}. Also the hook's own pre-send refusal. */
33
+ export const UPLOAD_BYTES_TOO_LARGE_ERROR = 'file exceeds the maximum upload size';
34
+ /** Not PNG / WebP / JPEG by magic bytes. */
35
+ export const UPLOAD_BYTES_TYPE_NOT_ALLOWED_ERROR = 'file type is not allowed';
36
+ /** The host's rolling window is full. Retryable. */
37
+ export const UPLOAD_BYTES_BUSY_ERROR = 'busy';
38
+ /** The host holds no block token. */
39
+ export const UPLOAD_BYTES_NO_TOKEN_ERROR = 'no block token';
40
+ /** `Image.name`'s bound in the host's persist input. */
41
+ const UPLOAD_BYTES_MAX_FILENAME_LENGTH = 255;
42
+ /**
43
+ * Which `OPEN_IMAGE_UPLOAD` variant a raw payload is, by the host's predicate
44
+ * (`resolveImageUploadBytes`). `none`: no `bytes` (null or absent), so the
45
+ * picker paths run unchanged. `invalid`: `bytes` is not an `ArrayBuffer` (a
46
+ * typed-array view is refused, not unwrapped), is empty, or comes with
47
+ * `purpose: 'generationSource'`.
48
+ */
49
+ export function resolveUploadBytesRequest(raw) {
50
+ if (raw.bytes == null)
51
+ return { kind: 'none' };
52
+ if (!(raw.bytes instanceof ArrayBuffer) || raw.bytes.byteLength === 0)
53
+ return { kind: 'invalid' };
54
+ if (raw.purpose === 'generationSource')
55
+ return { kind: 'invalid' };
56
+ return {
57
+ kind: 'bytes',
58
+ bytes: raw.bytes,
59
+ filename: typeof raw.filename === 'string' ? raw.filename : undefined,
60
+ };
61
+ }
62
+ function windowHasRoom(live, size) {
63
+ const total = live.reduce((sum, e) => sum + e.size, 0);
64
+ return live.length < UPLOAD_BYTES_MAX_PER_WINDOW && total + size <= UPLOAD_BYTES_MAX_BYTES_PER_WINDOW;
65
+ }
66
+ /** The host's stored name: the save-bytes cleaning, the sniffed extension, then 255 characters. */
67
+ function uploadBytesFilename(raw, type) {
68
+ const name = forceSaveBytesExtension(sanitizeSaveBytesFilename(raw), type);
69
+ if (name.length <= UPLOAD_BYTES_MAX_FILENAME_LENGTH)
70
+ return name;
71
+ const ext = name.slice(name.lastIndexOf('.'));
72
+ return name.slice(0, UPLOAD_BYTES_MAX_FILENAME_LENGTH - ext.length) + ext;
73
+ }
74
+ /**
75
+ * The host's admission decision (`processUploadBytes`), in its order:
76
+ * 1. the per-file cap, FIRST, so an over-cap file is always too large and
77
+ * never `busy` (a retry could never succeed);
78
+ * 2. the window, NOT recording, so a full window refuses `busy` before any
79
+ * sniffing;
80
+ * 3. PNG / WebP / JPEG by magic bytes;
81
+ * 4. record the upload in the window. Only an upload that will run counts.
82
+ * Returns the pruned window to store back.
83
+ */
84
+ export function admitUploadBytes(req, recent, now) {
85
+ const size = req.bytes.byteLength;
86
+ const live = recent.filter((e) => now - e.at < UPLOAD_BYTES_WINDOW_MS);
87
+ if (size > UPLOAD_BYTES_MAX_BYTES) {
88
+ return { result: { ok: false, error: UPLOAD_BYTES_TOO_LARGE_ERROR }, recent: live };
89
+ }
90
+ if (!windowHasRoom(live, size)) {
91
+ return { result: { ok: false, error: UPLOAD_BYTES_BUSY_ERROR }, recent: live };
92
+ }
93
+ const type = sniffImageBytes(new Uint8Array(req.bytes));
94
+ if (!type) {
95
+ return { result: { ok: false, error: UPLOAD_BYTES_TYPE_NOT_ALLOWED_ERROR }, recent: live };
96
+ }
97
+ return {
98
+ result: { ok: true, type, filename: uploadBytesFilename(req.filename, type) },
99
+ recent: [...live, { at: now, size }],
100
+ };
101
+ }
102
+ //# sourceMappingURL=uploadBytes.js.map
@@ -248,6 +248,7 @@ export declare function isValidImageUploadResult(p: unknown): p is {
248
248
  imageId: number;
249
249
  url: string;
250
250
  };
251
+ error?: string;
251
252
  };
252
253
  /**
253
254
  * Host→block `IMAGE_SCAN_RESOLVED` — the async scan verdict for a pending
@@ -474,7 +475,7 @@ export declare function isValidSharedReportResult(p: unknown): boolean;
474
475
  /**
475
476
  * Reply to `SAVE_IMAGE` (host download bridge). On success `ok` is a boolean;
476
477
  * `error` is a free-text host-side failure (disallowed origin / withheld image
477
- * / over-size / fetch failure). Uniform `{ ok, error }` contract (module
478
+ * / disallowed `bytes` type / over-size / fetch failure / `busy`). Uniform `{ ok, error }` contract (module
478
479
  * header) — same shape as `isValidSharedReportResult`.
479
480
  */
480
481
  export declare function isValidSaveImageResult(p: unknown): boolean;
@@ -657,6 +657,13 @@ export function isValidImageUploadResult(p) {
657
657
  return false;
658
658
  if (!isWireRequestIdShape(p.requestId))
659
659
  return false;
660
+ // `error` (civitai/civitai#5639): a refused or failed `OPEN_IMAGE_UPLOAD
661
+ // { bytes }`. Free text (host strings plus forwarded server messages), so
662
+ // only its TYPE is checked. 🔴 Accepting it is load-bearing: a validator that
663
+ // dropped `{ requestId, error }` would leave `useUploadImageBytes()` waiting
664
+ // out its whole timeout on a refusal the host already sent.
665
+ if (p.error !== undefined && typeof p.error !== 'string')
666
+ return false;
660
667
  // `selected` absent → cancelled upload (valid). When present, it must match
661
668
  // one of the purpose-keyed shapes (moderated / generationSource / pending).
662
669
  if (p.selected !== undefined) {
@@ -1584,7 +1591,7 @@ export function isValidSharedReportResult(p) {
1584
1591
  /**
1585
1592
  * Reply to `SAVE_IMAGE` (host download bridge). On success `ok` is a boolean;
1586
1593
  * `error` is a free-text host-side failure (disallowed origin / withheld image
1587
- * / over-size / fetch failure). Uniform `{ ok, error }` contract (module
1594
+ * / disallowed `bytes` type / over-size / fetch failure / `busy`). Uniform `{ ok, error }` contract (module
1588
1595
  * header) — same shape as `isValidSharedReportResult`.
1589
1596
  */
1590
1597
  export function isValidSaveImageResult(p) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/blocks-react",
3
- "version": "0.65.2",
3
+ "version": "0.67.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,8 +37,8 @@
37
37
  "node": ">=20"
38
38
  },
39
39
  "dependencies": {
40
- "@civitai/theme": "^0.5.2",
41
- "@civitai/components": "^0.9.3"
40
+ "@civitai/components": "^0.9.3",
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.",
44
44
  "peerDependencies": {
@@ -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.60.0"
61
+ "@civitai/app-sdk": "^0.62.0"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public"