@civitai/blocks-react 0.67.1 → 0.69.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.
@@ -69,6 +69,15 @@ export type PickerSelection = {
69
69
  } | {
70
70
  channel: 'RESOURCE_PICKER_RESULT';
71
71
  selected: BlockResourceInfo;
72
+ }
73
+ /**
74
+ * MULTI-select (`OPEN_RESOURCE_PICKER` with `multiple`): the staged resources
75
+ * in the order they were staged. Only ever produced when
76
+ * {@link OpenPickerOptions.multiple} is set.
77
+ */
78
+ | {
79
+ channel: 'RESOURCE_PICKER_RESULT';
80
+ selectedResources: BlockResourceInfo[];
72
81
  };
73
82
  export interface OpenPickerOptions {
74
83
  /**
@@ -104,6 +113,16 @@ export interface OpenPickerOptions {
104
113
  baseModelGroup?: string;
105
114
  /** Currently-selected versionId so the overlay can pre-highlight it. */
106
115
  currentVersionId?: number;
116
+ /**
117
+ * MULTI-select mode (`OPEN_RESOURCE_PICKER` with `multiple: { max }`). A card
118
+ * click STAGES (or un-stages) that resource instead of resolving; the "Add"
119
+ * button resolves with everything staged, in staging order, as
120
+ * `selectedResources`. At most `max` can be staged. Only meaningful on the
121
+ * `RESOURCE_PICKER_RESULT` channel — the checkpoint channel has no list reply.
122
+ */
123
+ multiple?: {
124
+ max: number;
125
+ };
107
126
  /** Advisory-SFW for the PUBLIC fallback read. Defaults true (fail-closed SFW). */
108
127
  anonSfwOnly?: boolean;
109
128
  /** The document to mount into. Defaults to `globalThis.document`. */
@@ -129,6 +148,19 @@ export interface PickerOverlayHandle {
129
148
  selectByVersionId(versionId: number): void;
130
149
  /** Dismiss without a selection. Resolves `null` + tears down. */
131
150
  dismiss(): void;
151
+ /**
152
+ * MULTI-select only: stage the card with this versionId, or un-stage it if it
153
+ * already is. No-op when absent, when not in multi-select mode, or when
154
+ * staging would exceed `max`. Does NOT resolve.
155
+ */
156
+ toggleStagedByVersionId(versionId: number): void;
157
+ /** MULTI-select only: the staged versionIds, in staging order. */
158
+ readonly stagedVersionIds: readonly number[];
159
+ /**
160
+ * MULTI-select only: resolve with everything staged (in staging order) and
161
+ * tear down. No-op while nothing is staged, or outside multi-select mode.
162
+ */
163
+ commitStaged(): void;
132
164
  /** Force teardown (host teardown path). Resolves `null` if not already resolved. */
133
165
  close(): void;
134
166
  /** True once the overlay has resolved (selection or dismissal). */
@@ -115,7 +115,12 @@ export function openPickerOverlay(opts) {
115
115
  let grid = null;
116
116
  let statusEl = null;
117
117
  let searchEl = null;
118
+ let commitBtn = null;
118
119
  let sentinel = null;
120
+ // --- Multi-select state. `staged` is in STAGING order — that order is the
121
+ // reply's order, so it is an array and never a Set keyed for lookup. ---
122
+ const multiMax = opts.multiple && opts.resultChannel === 'RESOURCE_PICKER_RESULT' ? opts.multiple.max : null;
123
+ let staged = [];
119
124
  let observer = null;
120
125
  // Separate observer that gates each thumbnail's `<img src>` (see observeThumb).
121
126
  let thumbObserver = null;
@@ -197,6 +202,7 @@ export function openPickerOverlay(opts) {
197
202
  grid = null;
198
203
  statusEl = null;
199
204
  searchEl = null;
205
+ commitBtn = null;
200
206
  };
201
207
  const resolve = (selection) => {
202
208
  if (resolved)
@@ -205,7 +211,52 @@ export function openPickerOverlay(opts) {
205
211
  teardown();
206
212
  opts.onResolve(selection);
207
213
  };
214
+ /** Repaint the staged outlines + the commit button from `staged`. */
215
+ const paintStaged = () => {
216
+ if (grid) {
217
+ for (const cell of Array.from(grid.querySelectorAll('[data-picker-card]'))) {
218
+ const id = Number(cell.getAttribute('data-picker-card'));
219
+ const isStaged = staged.some((c) => c.versionId === id);
220
+ cell.setAttribute('aria-pressed', String(isStaged));
221
+ cell.style.outline = isStaged ? '2px solid #5ec8a0' : '';
222
+ }
223
+ }
224
+ if (commitBtn) {
225
+ commitBtn.disabled = staged.length === 0;
226
+ commitBtn.textContent =
227
+ staged.length === 0 ? `Pick up to ${multiMax}` : `Add ${staged.length} of ${multiMax}`;
228
+ }
229
+ };
230
+ /** Multi-select: stage `card`, or un-stage it. Never exceeds `multiMax`. */
231
+ const toggleStaged = (card) => {
232
+ if (multiMax === null || resolved)
233
+ return;
234
+ if (staged.some((c) => c.versionId === card.versionId)) {
235
+ staged = staged.filter((c) => c.versionId !== card.versionId);
236
+ }
237
+ else if (staged.length < multiMax) {
238
+ staged = [...staged, card];
239
+ }
240
+ paintStaged();
241
+ };
242
+ const commitStaged = () => {
243
+ if (multiMax === null || staged.length === 0)
244
+ return;
245
+ resolve({
246
+ channel: 'RESOURCE_PICKER_RESULT',
247
+ selectedResources: staged.map((card) => cardToResource(card, opts.type)),
248
+ });
249
+ };
208
250
  const selectCard = (card) => {
251
+ // Multi-select: a programmatic "select" is the one-resource batch — still a
252
+ // LIST reply, because the reply shape follows the REQUEST, not the count.
253
+ if (multiMax !== null) {
254
+ resolve({
255
+ channel: 'RESOURCE_PICKER_RESULT',
256
+ selectedResources: [cardToResource(card, opts.type)],
257
+ });
258
+ return;
259
+ }
209
260
  // 🔴 BRANCH ON THE REPLY CHANNEL, NOT ON `opts.type` (#391). The channel is
210
261
  // what the block's consumers read the payload as; the requested type is
211
262
  // only what the catalog was filtered by. Branching on the type sent a
@@ -244,6 +295,17 @@ export function openPickerOverlay(opts) {
244
295
  dismiss() {
245
296
  resolve(null);
246
297
  },
298
+ toggleStagedByVersionId(versionId) {
299
+ const found = cards.find((c) => c.versionId === versionId);
300
+ if (found)
301
+ toggleStaged(found);
302
+ },
303
+ get stagedVersionIds() {
304
+ return staged.map((c) => c.versionId);
305
+ },
306
+ commitStaged() {
307
+ commitStaged();
308
+ },
247
309
  close() {
248
310
  resolve(null);
249
311
  },
@@ -312,7 +374,17 @@ export function openPickerOverlay(opts) {
312
374
  Object.assign(meta.style, META_STYLE);
313
375
  meta.textContent = [card.baseModel, card.versionName].filter(Boolean).join(' · ');
314
376
  cell.appendChild(meta);
315
- cell.addEventListener('click', () => selectCard(card));
377
+ if (multiMax !== null) {
378
+ // A rebuilt grid (new search) keeps what is already staged marked.
379
+ const isStaged = staged.some((c) => c.versionId === card.versionId);
380
+ cell.setAttribute('aria-pressed', String(isStaged));
381
+ if (isStaged)
382
+ cell.style.outline = '2px solid #5ec8a0';
383
+ cell.addEventListener('click', () => toggleStaged(card));
384
+ }
385
+ else {
386
+ cell.addEventListener('click', () => selectCard(card));
387
+ }
316
388
  return cell;
317
389
  };
318
390
  /**
@@ -524,7 +596,11 @@ export function openPickerOverlay(opts) {
524
596
  const title = doc.createElement('div');
525
597
  Object.assign(title.style, TITLE_STYLE);
526
598
  title.textContent =
527
- opts.type === 'LORA' ? 'Pick a LoRA (dev:live)' : 'Pick a checkpoint (dev:live)';
599
+ multiMax !== null
600
+ ? `Pick up to ${multiMax} LoRAs (dev:live)`
601
+ : opts.type === 'LORA'
602
+ ? 'Pick a LoRA (dev:live)'
603
+ : 'Pick a checkpoint (dev:live)';
528
604
  const closeBtn = doc.createElement('button');
529
605
  closeBtn.type = 'button';
530
606
  closeBtn.setAttribute('aria-label', 'Close picker');
@@ -563,6 +639,16 @@ export function openPickerOverlay(opts) {
563
639
  modal.appendChild(searchEl);
564
640
  modal.appendChild(statusEl);
565
641
  modal.appendChild(grid);
642
+ if (multiMax !== null) {
643
+ // Multi-select commit bar. A card click only stages; this resolves.
644
+ commitBtn = doc.createElement('button');
645
+ commitBtn.type = 'button';
646
+ commitBtn.setAttribute('data-picker-commit', '');
647
+ Object.assign(commitBtn.style, COMMIT_STYLE);
648
+ commitBtn.addEventListener('click', () => commitStaged());
649
+ modal.appendChild(commitBtn);
650
+ paintStaged();
651
+ }
566
652
  root.appendChild(modal);
567
653
  // Escape = dismiss.
568
654
  root.addEventListener('keydown', (e) => {
@@ -611,6 +697,17 @@ const TITLE_STYLE = {
611
697
  fontSize: '15px',
612
698
  fontWeight: '600',
613
699
  };
700
+ const COMMIT_STYLE = {
701
+ margin: '10px 14px 14px',
702
+ padding: '8px 14px',
703
+ alignSelf: 'flex-end',
704
+ border: '1px solid #2f6f5a',
705
+ borderRadius: '6px',
706
+ background: '#1f4a3c',
707
+ color: '#e6edf3',
708
+ font: 'inherit',
709
+ cursor: 'pointer',
710
+ };
614
711
  const CLOSE_STYLE = {
615
712
  background: 'transparent',
616
713
  border: 'none',
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The `dev:live` check of a `TRACK_EVENT` against the manifest's
3
+ * `analytics.events` declaration.
4
+ *
5
+ * Production stores an event only when the APPROVED manifest declares it, and
6
+ * keeps only the declared properties whose value has the declared type. An
7
+ * undeclared event is counted as a drop and its name is not stored; an
8
+ * undeclared property, or a value of the wrong type, is dropped from the row.
9
+ * None of that reaches the block: `TRACK_EVENT` is fire-and-forget, so a typo
10
+ * in an event name is invisible until an owner wonders why the analytics panel
11
+ * is empty. This check makes the same three decisions locally and returns one
12
+ * actionable message per DISTINCT problem, so the console says it once.
13
+ *
14
+ * It is a dev aid, not the contract. The server is authoritative, and the
15
+ * dev server's manifest validation (the schema) is what reports a malformed
16
+ * declaration in detail; this reader only needs enough structure to look a
17
+ * name up.
18
+ *
19
+ * Total: never throws, whatever the manifest or the payload holds.
20
+ */
21
+ export interface TrackEventChecker {
22
+ /**
23
+ * The messages for this event that have not been returned before. Empty for a
24
+ * declared, valid event, and for any problem already reported.
25
+ */
26
+ check(payload: unknown): string[];
27
+ }
28
+ /**
29
+ * @param manifest The app's manifest as the harness imported it
30
+ * (`import manifest from '../block.manifest.json'`). `undefined` = the harness
31
+ * passed none, so nothing can be checked; that is said once.
32
+ */
33
+ export declare function createTrackEventChecker(manifest: unknown): TrackEventChecker;
34
+ //# sourceMappingURL=trackEventCheck.d.ts.map
@@ -0,0 +1,194 @@
1
+ /**
2
+ * The `dev:live` check of a `TRACK_EVENT` against the manifest's
3
+ * `analytics.events` declaration.
4
+ *
5
+ * Production stores an event only when the APPROVED manifest declares it, and
6
+ * keeps only the declared properties whose value has the declared type. An
7
+ * undeclared event is counted as a drop and its name is not stored; an
8
+ * undeclared property, or a value of the wrong type, is dropped from the row.
9
+ * None of that reaches the block: `TRACK_EVENT` is fire-and-forget, so a typo
10
+ * in an event name is invisible until an owner wonders why the analytics panel
11
+ * is empty. This check makes the same three decisions locally and returns one
12
+ * actionable message per DISTINCT problem, so the console says it once.
13
+ *
14
+ * It is a dev aid, not the contract. The server is authoritative, and the
15
+ * dev server's manifest validation (the schema) is what reports a malformed
16
+ * declaration in detail; this reader only needs enough structure to look a
17
+ * name up.
18
+ *
19
+ * Total: never throws, whatever the manifest or the payload holds.
20
+ */
21
+ /** Where every message points the author. */
22
+ const WHERE = '`analytics.events` in block.manifest.json';
23
+ /** The event/property name rule (the manifest schema's `patternProperties` key). */
24
+ const NAME_RE = /^[a-z][a-z0-9_]{0,63}$/;
25
+ function isPlainObject(value) {
26
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
27
+ }
28
+ /** A name or value as it appears in a message: JSON-quoted and bounded. */
29
+ function quote(value) {
30
+ let text;
31
+ try {
32
+ text = typeof value === 'string' ? value : String(JSON.stringify(value));
33
+ }
34
+ catch {
35
+ text = Object.prototype.toString.call(value);
36
+ }
37
+ if (text.length > 80)
38
+ text = `${text.slice(0, 80)}…`;
39
+ return typeof value === 'string' ? JSON.stringify(text) : text;
40
+ }
41
+ /** Reads the declaration, or says why it cannot. An unreadable one declares nothing. */
42
+ function readDeclarations(manifest) {
43
+ const none = new Map();
44
+ if (!isPlainObject(manifest))
45
+ return { declarations: none, problem: 'the manifest is not an object' };
46
+ const analytics = manifest.analytics;
47
+ if (analytics === undefined)
48
+ return { declarations: none };
49
+ if (!isPlainObject(analytics))
50
+ return { declarations: none, problem: '`analytics` is not an object' };
51
+ const events = analytics.events;
52
+ if (events === undefined)
53
+ return { declarations: none };
54
+ if (!isPlainObject(events))
55
+ return { declarations: none, problem: '`analytics.events` is not an object' };
56
+ const out = new Map();
57
+ for (const [eventName, rawEvent] of Object.entries(events)) {
58
+ if (!isPlainObject(rawEvent)) {
59
+ return { declarations: none, problem: `event ${quote(eventName)} is not an object` };
60
+ }
61
+ const props = new Map();
62
+ const rawProps = rawEvent.properties;
63
+ if (rawProps !== undefined && !isPlainObject(rawProps)) {
64
+ return { declarations: none, problem: `${quote(eventName)}.properties is not an object` };
65
+ }
66
+ for (const [propName, rawProp] of Object.entries(rawProps ?? {})) {
67
+ const at = `${quote(eventName)}.properties.${propName}`;
68
+ if (!isPlainObject(rawProp))
69
+ return { declarations: none, problem: `${at} is not an object` };
70
+ if (rawProp.type === 'number' || rawProp.type === 'boolean') {
71
+ props.set(propName, { type: rawProp.type });
72
+ }
73
+ else if (rawProp.type === 'enum') {
74
+ const values = rawProp.values;
75
+ if (!Array.isArray(values) || !values.every((v) => typeof v === 'string')) {
76
+ return { declarations: none, problem: `${at}.values is not an array of strings` };
77
+ }
78
+ props.set(propName, { type: 'enum', values: new Set(values) });
79
+ }
80
+ else {
81
+ return {
82
+ declarations: none,
83
+ problem: `${at}.type is ${quote(rawProp.type)}, not one of enum, number, boolean`,
84
+ };
85
+ }
86
+ }
87
+ out.set(eventName, props);
88
+ }
89
+ return { declarations: out };
90
+ }
91
+ /** What a declared property accepts, for a message. */
92
+ function describeDeclared(prop) {
93
+ if (prop.type === 'enum')
94
+ return `one of ${[...prop.values].map((v) => quote(v)).join(', ')}`;
95
+ return prop.type === 'number' ? 'a finite number' : 'a boolean';
96
+ }
97
+ /** Whether production keeps `value` for `prop` (mirrors the ingest's classification). */
98
+ function accepts(prop, value) {
99
+ if (prop.type === 'enum')
100
+ return typeof value === 'string' && prop.values.has(value);
101
+ if (prop.type === 'number')
102
+ return typeof value === 'number' && Number.isFinite(value);
103
+ return typeof value === 'boolean';
104
+ }
105
+ /**
106
+ * @param manifest The app's manifest as the harness imported it
107
+ * (`import manifest from '../block.manifest.json'`). `undefined` = the harness
108
+ * passed none, so nothing can be checked; that is said once.
109
+ */
110
+ export function createTrackEventChecker(manifest) {
111
+ const reported = new Set();
112
+ const once = (key, message, out) => {
113
+ if (reported.has(key))
114
+ return;
115
+ reported.add(key);
116
+ out.push(message);
117
+ };
118
+ const hasManifest = manifest !== undefined;
119
+ let read = { declarations: new Map() };
120
+ if (hasManifest) {
121
+ try {
122
+ read = readDeclarations(manifest);
123
+ }
124
+ catch {
125
+ // Reachable only via a throwing getter or Proxy; an imported JSON file cannot get here.
126
+ read = { declarations: new Map(), problem: 'reading it threw' };
127
+ }
128
+ }
129
+ const { declarations, problem } = read;
130
+ const declaresAny = declarations.size > 0;
131
+ return {
132
+ check(payload) {
133
+ const out = [];
134
+ try {
135
+ if (!hasManifest) {
136
+ once('no-manifest', 'TRACK_EVENT is not checked against your manifest: pass `manifest` to createLiveHost ' +
137
+ "(import it from block.manifest.json) to be warned here about events production will drop.", out);
138
+ return out;
139
+ }
140
+ if (problem) {
141
+ once('unreadable', `${WHERE} could not be read (${problem}). Production accepts events only against a valid ` +
142
+ 'declaration, so it would record none of them; the dev server\'s manifest validation names the problem.', out);
143
+ }
144
+ const body = isPlainObject(payload) ? payload : {};
145
+ const eventName = body.eventName;
146
+ if (typeof eventName !== 'string') {
147
+ once('bad-name', `track() was called with a non-string event name (${quote(eventName)}). Production counts it as a dropped event.`, out);
148
+ return out;
149
+ }
150
+ const declaredProps = declarations.get(eventName);
151
+ if (!declaredProps) {
152
+ const hint = NAME_RE.test(eventName)
153
+ ? ''
154
+ : ' (event names are lowercase snake_case: a letter, then up to 63 of a-z, 0-9 or _)';
155
+ const declared = declaresAny || problem ? '' : ' Your manifest declares no events yet.';
156
+ once(`event:${eventName}`, `track(${quote(eventName)}) is not declared in ${WHERE}${hint}. Production counts it as a dropped ` +
157
+ `event and does not store its name or properties.${declared} Declare it to record it.`, out);
158
+ return out;
159
+ }
160
+ const props = body.properties;
161
+ if (props === undefined || props === null)
162
+ return out;
163
+ if (!isPlainObject(props)) {
164
+ once(`props-shape:${eventName}`, `track(${quote(eventName)}) was sent properties that are not an object (${quote(props)}). ` +
165
+ 'Production keeps none of them.', out);
166
+ return out;
167
+ }
168
+ for (const [propName, value] of Object.entries(props)) {
169
+ // JSON drops an `undefined` value, so the host never forwards one.
170
+ if (value === undefined)
171
+ continue;
172
+ const declared = declaredProps.get(propName);
173
+ if (!declared) {
174
+ once(`prop:${eventName}.${propName}`, `track(${quote(eventName)}) property ${quote(propName)} is not declared under ` +
175
+ `${quote(eventName)}.properties in ${WHERE}. Production drops it from the event.`, out);
176
+ continue;
177
+ }
178
+ if (!accepts(declared, value)) {
179
+ once(`value:${eventName}.${propName}`, `track(${quote(eventName)}) property ${quote(propName)} is ${quote(value)}, but ${WHERE} ` +
180
+ `declares it as ${declared.type} (${describeDeclared(declared)}). Production drops the value.` +
181
+ (typeof value === 'string' && declared.type === 'enum'
182
+ ? ' Free-text strings are never stored: only the declared enum values are.'
183
+ : ''), out);
184
+ }
185
+ }
186
+ }
187
+ catch {
188
+ // A throwing getter or Proxy in a payload. The check is advisory: say nothing.
189
+ }
190
+ return out;
191
+ },
192
+ };
193
+ }
194
+ //# sourceMappingURL=trackEventCheck.js.map
@@ -131,6 +131,9 @@ const REQUEST_TIMEOUT_CLASS = {
131
131
  // longer bound (`WORKFLOW_REQUEST_TIMEOUT_MS` in `hooks/useBuzzWorkflow.ts`)
132
132
  // for orchestrator latency — a different reason for a different number.
133
133
  ESTIMATE_WORKFLOW: 'protocol',
134
+ // The batch twin of ESTIMATE_WORKFLOW: no person in the loop, up to 16 cost
135
+ // quotes behind one reply. Sent under the same `WORKFLOW_REQUEST_TIMEOUT_MS`.
136
+ ESTIMATE_WORKFLOW_BATCH: 'protocol',
134
137
  POLL_WORKFLOW: 'protocol',
135
138
  SUBMIT_WORKFLOW: 'protocol',
136
139
  GET_BUZZ_ACCOUNTS: 'protocol',
@@ -53,7 +53,7 @@
53
53
  * helpers. Do not open-code the test again: the two spellings coexisted in one
54
54
  * file for a release precisely because it was inline at every call site.
55
55
  */
56
- import type { BlockInitPayload, BlockWorkflowSnapshot, Theme, WrappedToken } from '@civitai/app-sdk/blocks';
56
+ import type { BlockInitPayload, BlockWorkflowSnapshot, SharedStorageRateLimit, Theme, WrappedToken } from '@civitai/app-sdk/blocks';
57
57
  /**
58
58
  * Shape check for the wrapped-token shape carried by `BLOCK_INIT.token`,
59
59
  * `TOKEN_REFRESH.payload.token`, and `TOKEN_REFRESH_RESPONSE.payload.token`.
@@ -175,6 +175,24 @@ export declare function isValidWorkflowReply(p: unknown): p is {
175
175
  snapshot: BlockWorkflowSnapshot;
176
176
  requestId?: string;
177
177
  };
178
+ /**
179
+ * Reply to a block-initiated `ESTIMATE_WORKFLOW_BATCH`. A well-formed reply
180
+ * carries EITHER an `error` string OR `snapshots` + `aggregate`.
181
+ *
182
+ * 🔴 A PRESENT `error` IS ACCEPTED ON ITS OWN, unlike the single-estimate reply
183
+ * ({@link isValidWorkflowReply}, which requires a snapshot). That is what lets
184
+ * the host's generic `{ requestId, error: 'unsupported on this host' }` reach
185
+ * `useBatchEstimate`, which turns it into the typed error an app falls back to
186
+ * per-cell estimates on. Dropping it would leave the block waiting out the
187
+ * timeout instead. `error` is shape-checked only: the channel carries free-text
188
+ * server messages.
189
+ *
190
+ * Without an `error`, EVERY cell must be a valid workflow snapshot and the
191
+ * aggregate must hold three finite numbers. One malformed cell rejects the whole
192
+ * reply, the same way one malformed snapshot rejects a single-estimate reply:
193
+ * the block dereferences these fields.
194
+ */
195
+ export declare function isValidEstimateBatchResult(p: unknown): boolean;
178
196
  export declare function isValidBuzzPurchaseResult(p: unknown): p is {
179
197
  purchased: boolean;
180
198
  newBalance?: number;
@@ -494,6 +512,24 @@ export declare function isValidSharedAppendResult(p: unknown): boolean;
494
512
  * error path is `{ error }`. Uniform `{ ok, error }` contract (module header).
495
513
  */
496
514
  export declare function isValidSharedWithdrawResult(p: unknown): boolean;
515
+ /**
516
+ * Validate the `rateLimit` a host put on a shared-storage refusal. Returns a
517
+ * copy holding ONLY the two known fields, or `undefined` when `value` is
518
+ * missing or malformed.
519
+ *
520
+ * Kept in lockstep with the platform's own parser for the same metadata
521
+ * (`parseRateLimitMeta` in civitai's `src/shared/utils/rate-limit-meta.ts`):
522
+ * `retryAfterSeconds` must be an integer of at least 1, and `retryAt` a string
523
+ * `Date.parse` can read. Anything else — a wrong type, a negative or fractional
524
+ * number, a missing field, a non-object — is malformed.
525
+ *
526
+ * 🔴 NOT A VALIDATOR, deliberately. None of the `isValidShared*Result`
527
+ * predicates look at `rateLimit`, so a malformed one can never DROP a reply:
528
+ * dropping it would hang the block's request to its timeout over an optional
529
+ * hint. The reply is delivered unchanged, and `useSharedStorage` calls this
530
+ * parser: a malformed value is ignored and the plain refusal error is thrown.
531
+ */
532
+ export declare function parseSharedStorageRateLimit(value: unknown): SharedStorageRateLimit | undefined;
497
533
  /**
498
534
  * Reply to `OPEN_CHECKPOINT_PICKER`. `selected` is ABSENT when the user
499
535
  * dismissed the picker (the hook resolves `{ selected: undefined }`); when
@@ -501,11 +537,18 @@ export declare function isValidSharedWithdrawResult(p: unknown): boolean;
501
537
  */
502
538
  export declare function isValidCheckpointPickerResult(p: unknown): boolean;
503
539
  /**
504
- * Reply to `OPEN_RESOURCE_PICKER`. `selected` is ABSENT on dismiss (the hook
505
- * resolves `null`); when present it is a `BlockResourceInfo` projection — the 5
506
- * base fields plus `modelType` and the OPTIONAL public recommended-settings
507
- * (`strength`/`minStrength`/`maxStrength` finite, `trainedWords` a string[],
508
- * `clipSkip` a finite number OR `null`).
540
+ * Reply to `OPEN_RESOURCE_PICKER`.
541
+ *
542
+ * Single pick: `selected` is ABSENT on dismiss (the hook resolves `null`); when
543
+ * present it is a `BlockResourceInfo` projection.
544
+ *
545
+ * Multi-select: `selectedResources` is an array of the same projection (`[]` on
546
+ * dismiss). ONE malformed entry drops the whole reply — each `versionId` is
547
+ * money-adjacent, and handing the caller the well-formed remainder would
548
+ * silently change which resources a generation runs with.
549
+ *
550
+ * `error` (a refusal) is not validated here: the hook reads it by PRESENCE, and
551
+ * a reply carrying only `{ requestId, error }` must reach it.
509
552
  */
510
553
  export declare function isValidResourcePickerResult(p: unknown): boolean;
511
554
  /**