@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.
@@ -232,6 +232,13 @@ export function isValidWorkflowSnapshot(s) {
232
232
  if (s.cost !== undefined) {
233
233
  if (!isObject(s.cost) || typeof s.cost.total !== 'number')
234
234
  return false;
235
+ // OPTIONAL: an older host omits it and the snapshot stays valid. Present, it
236
+ // is subtracted from `total` by the block, so a non-number or a negative
237
+ // would render a wrong price.
238
+ if (s.cost.authorFee !== undefined &&
239
+ (!isFiniteNumber(s.cost.authorFee) || s.cost.authorFee < 0)) {
240
+ return false;
241
+ }
235
242
  }
236
243
  if (s.imageUrls !== undefined) {
237
244
  if (!Array.isArray(s.imageUrls))
@@ -241,6 +248,12 @@ export function isValidWorkflowSnapshot(s) {
241
248
  }
242
249
  if (s.error !== undefined && typeof s.error !== 'string')
243
250
  return false;
251
+ // OPTIONAL: set only on the refusals that define a code (today the training
252
+ // price-quote refusal), absent everywhere else. Checked as a STRING, not
253
+ // against today's codes — the open-ended-union rule below: a host adding a
254
+ // code must not turn into a dropped reply in every shipped bundle.
255
+ if (s.errorCode !== undefined && typeof s.errorCode !== 'string')
256
+ return false;
244
257
  if (s.autoClaim !== undefined) {
245
258
  if (!isObject(s.autoClaim))
246
259
  return false;
@@ -513,6 +526,44 @@ export function isValidWorkflowReply(p) {
513
526
  return false;
514
527
  return true;
515
528
  }
529
+ /**
530
+ * Reply to a block-initiated `ESTIMATE_WORKFLOW_BATCH`. A well-formed reply
531
+ * carries EITHER an `error` string OR `snapshots` + `aggregate`.
532
+ *
533
+ * 🔴 A PRESENT `error` IS ACCEPTED ON ITS OWN, unlike the single-estimate reply
534
+ * ({@link isValidWorkflowReply}, which requires a snapshot). That is what lets
535
+ * the host's generic `{ requestId, error: 'unsupported on this host' }` reach
536
+ * `useBatchEstimate`, which turns it into the typed error an app falls back to
537
+ * per-cell estimates on. Dropping it would leave the block waiting out the
538
+ * timeout instead. `error` is shape-checked only: the channel carries free-text
539
+ * server messages.
540
+ *
541
+ * Without an `error`, EVERY cell must be a valid workflow snapshot and the
542
+ * aggregate must hold three finite numbers. One malformed cell rejects the whole
543
+ * reply, the same way one malformed snapshot rejects a single-estimate reply:
544
+ * the block dereferences these fields.
545
+ */
546
+ export function isValidEstimateBatchResult(p) {
547
+ if (!isObject(p))
548
+ return false;
549
+ if (!isWireRequestIdShape(p.requestId))
550
+ return false;
551
+ if (p.error !== undefined)
552
+ return typeof p.error === 'string';
553
+ if (!Array.isArray(p.snapshots))
554
+ return false;
555
+ if (!p.snapshots.every((s) => isValidWorkflowSnapshot(s)))
556
+ return false;
557
+ const a = p.aggregate;
558
+ if (!isObject(a))
559
+ return false;
560
+ for (const key of ['total', 'pricedCells', 'cellCount']) {
561
+ const value = a[key];
562
+ if (typeof value !== 'number' || !Number.isFinite(value))
563
+ return false;
564
+ }
565
+ return true;
566
+ }
516
567
  export function isValidBuzzPurchaseResult(p) {
517
568
  if (!isObject(p))
518
569
  return false;
@@ -1478,6 +1529,10 @@ export function isValidAppStorageQuotaResult(p) {
1478
1529
  // uniform). Closes the silent-corrupt-return hole: without these,
1479
1530
  // `getCount/getCounts/append/vote/unvote`/`list` returned a missing field as
1480
1531
  // `undefined` typed `number`/`string` with no throw, no timeout.
1532
+ //
1533
+ // The optional `rateLimit` on the six write replies is NOT checked by any
1534
+ // predicate here, so it can never drop a reply; the hook parses it (see
1535
+ // `parseSharedStorageRateLimit` below the validators).
1481
1536
  /** The moderated `{ title, body?, data? }` value carried by each SHARED entry. */
1482
1537
  function isValidSharedValue(v) {
1483
1538
  if (!isObject(v))
@@ -1676,6 +1731,36 @@ export function isValidSharedWithdrawResult(p) {
1676
1731
  return false;
1677
1732
  return true;
1678
1733
  }
1734
+ /**
1735
+ * Validate the `rateLimit` a host put on a shared-storage refusal. Returns a
1736
+ * copy holding ONLY the two known fields, or `undefined` when `value` is
1737
+ * missing or malformed.
1738
+ *
1739
+ * Kept in lockstep with the platform's own parser for the same metadata
1740
+ * (`parseRateLimitMeta` in civitai's `src/shared/utils/rate-limit-meta.ts`):
1741
+ * `retryAfterSeconds` must be an integer of at least 1, and `retryAt` a string
1742
+ * `Date.parse` can read. Anything else — a wrong type, a negative or fractional
1743
+ * number, a missing field, a non-object — is malformed.
1744
+ *
1745
+ * 🔴 NOT A VALIDATOR, deliberately. None of the `isValidShared*Result`
1746
+ * predicates look at `rateLimit`, so a malformed one can never DROP a reply:
1747
+ * dropping it would hang the block's request to its timeout over an optional
1748
+ * hint. The reply is delivered unchanged, and `useSharedStorage` calls this
1749
+ * parser: a malformed value is ignored and the plain refusal error is thrown.
1750
+ */
1751
+ export function parseSharedStorageRateLimit(value) {
1752
+ if (!isObject(value))
1753
+ return undefined;
1754
+ const { retryAfterSeconds, retryAt } = value;
1755
+ if (typeof retryAfterSeconds !== 'number' || !Number.isInteger(retryAfterSeconds)) {
1756
+ return undefined;
1757
+ }
1758
+ if (retryAfterSeconds < 1)
1759
+ return undefined;
1760
+ if (typeof retryAt !== 'string' || Number.isNaN(Date.parse(retryAt)))
1761
+ return undefined;
1762
+ return { retryAfterSeconds, retryAt };
1763
+ }
1679
1764
  // ============================================================
1680
1765
  // Picker + user-checkpoint reply validators
1681
1766
  // ============================================================
@@ -1723,42 +1808,65 @@ export function isValidCheckpointPickerResult(p) {
1723
1808
  return true;
1724
1809
  }
1725
1810
  /**
1726
- * Reply to `OPEN_RESOURCE_PICKER`. `selected` is ABSENT on dismiss (the hook
1727
- * resolves `null`); when present it is a `BlockResourceInfo` projection — the 5
1728
- * base fields plus `modelType` and the OPTIONAL public recommended-settings
1729
- * (`strength`/`minStrength`/`maxStrength` finite, `trainedWords` a string[],
1730
- * `clipSkip` a finite number OR `null`).
1811
+ * One `BlockResourceInfo` projection — the 5 base fields plus `modelType` and
1812
+ * the OPTIONAL public recommended-settings (`strength`/`minStrength`/
1813
+ * `maxStrength` finite, `trainedWords` a string[], `clipSkip` a finite number
1814
+ * OR `null`). Shared by the single-pick `selected` and every entry of the
1815
+ * multi-select `selectedResources`, so the two cannot drift on what a
1816
+ * well-formed resource is.
1817
+ */
1818
+ function isValidPickerResource(s) {
1819
+ if (!isObject(s))
1820
+ return false;
1821
+ if (!isValidPickerResourceBase(s))
1822
+ return false;
1823
+ if (s.modelType !== undefined && typeof s.modelType !== 'string')
1824
+ return false;
1825
+ if (s.strength !== undefined && !isFiniteNumber(s.strength))
1826
+ return false;
1827
+ if (s.minStrength !== undefined && !isFiniteNumber(s.minStrength))
1828
+ return false;
1829
+ if (s.maxStrength !== undefined && !isFiniteNumber(s.maxStrength))
1830
+ return false;
1831
+ if (s.trainedWords !== undefined) {
1832
+ if (!Array.isArray(s.trainedWords))
1833
+ return false;
1834
+ if (!s.trainedWords.every((w) => typeof w === 'string'))
1835
+ return false;
1836
+ }
1837
+ // `clipSkip` is `number | null` — accept a finite number OR null; reject
1838
+ // other present types.
1839
+ if (s.clipSkip !== undefined && s.clipSkip !== null && !isFiniteNumber(s.clipSkip)) {
1840
+ return false;
1841
+ }
1842
+ return true;
1843
+ }
1844
+ /**
1845
+ * Reply to `OPEN_RESOURCE_PICKER`.
1846
+ *
1847
+ * Single pick: `selected` is ABSENT on dismiss (the hook resolves `null`); when
1848
+ * present it is a `BlockResourceInfo` projection.
1849
+ *
1850
+ * Multi-select: `selectedResources` is an array of the same projection (`[]` on
1851
+ * dismiss). ONE malformed entry drops the whole reply — each `versionId` is
1852
+ * money-adjacent, and handing the caller the well-formed remainder would
1853
+ * silently change which resources a generation runs with.
1854
+ *
1855
+ * `error` (a refusal) is not validated here: the hook reads it by PRESENCE, and
1856
+ * a reply carrying only `{ requestId, error }` must reach it.
1731
1857
  */
1732
1858
  export function isValidResourcePickerResult(p) {
1733
1859
  if (!isObject(p))
1734
1860
  return false;
1735
1861
  if (!isWireRequestIdShape(p.requestId))
1736
1862
  return false;
1737
- if (p.selected !== undefined) {
1738
- const s = p.selected;
1739
- if (!isObject(s))
1740
- return false;
1741
- if (!isValidPickerResourceBase(s))
1742
- return false;
1743
- if (s.modelType !== undefined && typeof s.modelType !== 'string')
1744
- return false;
1745
- if (s.strength !== undefined && !isFiniteNumber(s.strength))
1746
- return false;
1747
- if (s.minStrength !== undefined && !isFiniteNumber(s.minStrength))
1863
+ if (p.selected !== undefined && !isValidPickerResource(p.selected))
1864
+ return false;
1865
+ if (p.selectedResources !== undefined) {
1866
+ if (!Array.isArray(p.selectedResources))
1748
1867
  return false;
1749
- if (s.maxStrength !== undefined && !isFiniteNumber(s.maxStrength))
1868
+ if (!p.selectedResources.every(isValidPickerResource))
1750
1869
  return false;
1751
- if (s.trainedWords !== undefined) {
1752
- if (!Array.isArray(s.trainedWords))
1753
- return false;
1754
- if (!s.trainedWords.every((w) => typeof w === 'string'))
1755
- return false;
1756
- }
1757
- // `clipSkip` is `number | null` — accept a finite number OR null; reject
1758
- // other present types.
1759
- if (s.clipSkip !== undefined && s.clipSkip !== null && !isFiniteNumber(s.clipSkip)) {
1760
- return false;
1761
- }
1762
1870
  }
1763
1871
  return true;
1764
1872
  }
@@ -1862,6 +1970,8 @@ export function payloadValidatorFor(type) {
1862
1970
  case 'WORKFLOW_STATUS':
1863
1971
  case 'WORKFLOW_CANCELED':
1864
1972
  return isValidWorkflowReply;
1973
+ case 'ESTIMATE_BATCH_RESULT':
1974
+ return isValidEstimateBatchResult;
1865
1975
  case 'BUZZ_PURCHASE_RESULT':
1866
1976
  return isValidBuzzPurchaseResult;
1867
1977
  case 'BUZZ_BALANCE_RESULT':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/blocks-react",
3
- "version": "0.67.1",
3
+ "version": "0.69.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",
@@ -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.63.0"
61
+ "@civitai/app-sdk": "^0.65.0"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public"