@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.
- package/README.md +170 -2
- package/dist/hooks/useCreatePostFromApp.d.ts +2 -1
- package/dist/hooks/useCreatePostFromApp.js +2 -1
- package/dist/hooks/useSaveImage.d.ts +53 -12
- package/dist/hooks/useSaveImage.js +31 -5
- package/dist/hooks/useUploadImageBytes.d.ts +84 -0
- package/dist/hooks/useUploadImageBytes.js +77 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/internal/liveHost.js +22 -4
- package/dist/internal/mockHost.d.ts +198 -38
- package/dist/internal/mockHost.js +197 -40
- package/dist/internal/saveBytes.d.ts +102 -0
- package/dist/internal/saveBytes.js +187 -0
- package/dist/internal/uploadBytes.d.ts +89 -0
- package/dist/internal/uploadBytes.js +102 -0
- package/dist/transport/validate.d.ts +2 -1
- package/dist/transport/validate.js +8 -1
- package/package.json +4 -4
|
@@ -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.
|
|
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/
|
|
41
|
-
"@civitai/
|
|
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.
|
|
61
|
+
"@civitai/app-sdk": "^0.62.0"
|
|
62
62
|
},
|
|
63
63
|
"publishConfig": {
|
|
64
64
|
"access": "public"
|