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