@civitai/blocks-react 0.56.0 → 0.57.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.
Files changed (262) hide show
  1. package/README.md +34 -2
  2. package/dist/api/generationResources.d.ts +4 -3
  3. package/dist/hooks/returnTypeLedger.d.ts +2 -0
  4. package/dist/hooks/returnTypeLedger.js +2 -0
  5. package/dist/hooks/useAppStorage.js +2 -2
  6. package/dist/hooks/useAppWorkflows.d.ts +5 -2
  7. package/dist/hooks/useAppWorkflows.js +18 -7
  8. package/dist/hooks/useBlockAnalytics.d.ts +5 -3
  9. package/dist/hooks/useBlockAnalytics.js +1 -1
  10. package/dist/hooks/useBlockBreakpoint.d.ts +7 -1
  11. package/dist/hooks/useBlockContext.d.ts +13 -2
  12. package/dist/hooks/useBlockContext.js +1 -1
  13. package/dist/hooks/useBlockResize.d.ts +7 -1
  14. package/dist/hooks/useBlockResize.js +1 -1
  15. package/dist/hooks/useBlockSettings.d.ts +6 -1
  16. package/dist/hooks/useBlockTheme.d.ts +6 -1
  17. package/dist/hooks/useBlockToken.d.ts +8 -3
  18. package/dist/hooks/useBlockToken.js +2 -2
  19. package/dist/hooks/useBuzzAccounts.d.ts +3 -2
  20. package/dist/hooks/useBuzzAccounts.js +16 -15
  21. package/dist/hooks/useBuzzBalance.d.ts +3 -2
  22. package/dist/hooks/useBuzzBalance.js +16 -18
  23. package/dist/hooks/useBuzzPurchase.d.ts +8 -6
  24. package/dist/hooks/useBuzzPurchase.js +3 -3
  25. package/dist/hooks/useBuzzTransactions.d.ts +4 -2
  26. package/dist/hooks/useBuzzTransactions.js +16 -15
  27. package/dist/hooks/useBuzzWorkflow.d.ts +71 -28
  28. package/dist/hooks/useBuzzWorkflow.js +31 -17
  29. package/dist/hooks/useCheckpointPicker.d.ts +18 -16
  30. package/dist/hooks/useCheckpointPicker.js +3 -3
  31. package/dist/hooks/useCivitaiNavigate.d.ts +5 -3
  32. package/dist/hooks/useCivitaiNavigate.js +1 -1
  33. package/dist/hooks/useCollectionFollow.js +4 -4
  34. package/dist/hooks/useConsentUnavailable.js +2 -2
  35. package/dist/hooks/useCreatePostFromApp.js +4 -4
  36. package/dist/hooks/useDailyCompensation.d.ts +4 -2
  37. package/dist/hooks/useDailyCompensation.js +17 -15
  38. package/dist/hooks/useDirectLoad.d.ts +7 -1
  39. package/dist/hooks/useDirectLoad.js +1 -1
  40. package/dist/hooks/useDomainMaturity.d.ts +6 -2
  41. package/dist/hooks/useGatedImages.d.ts +8 -0
  42. package/dist/hooks/useGatedImages.js +10 -2
  43. package/dist/hooks/useGenerationResources.d.ts +5 -3
  44. package/dist/hooks/useHostOrigin.d.ts +7 -1
  45. package/dist/hooks/useHostOrigin.js +1 -1
  46. package/dist/hooks/useImageUpload.d.ts +44 -17
  47. package/dist/hooks/useImageUpload.js +24 -4
  48. package/dist/hooks/usePublishGenerationOutputs.js +4 -4
  49. package/dist/hooks/useRequestConsent.d.ts +7 -5
  50. package/dist/hooks/useRequestConsent.js +1 -1
  51. package/dist/hooks/useRequestSequencer.d.ts +71 -0
  52. package/dist/hooks/useRequestSequencer.js +28 -0
  53. package/dist/hooks/useRequestSignIn.d.ts +7 -5
  54. package/dist/hooks/useRequestSignIn.js +1 -1
  55. package/dist/hooks/useResourcePicker.d.ts +16 -14
  56. package/dist/hooks/useResourcePicker.js +3 -3
  57. package/dist/hooks/useSaveImage.js +2 -2
  58. package/dist/hooks/useSharedStorage.js +2 -2
  59. package/dist/hooks/useTip.js +1 -1
  60. package/dist/hooks/useTipAllowance.d.ts +13 -1
  61. package/dist/hooks/useTipAllowance.js +49 -11
  62. package/dist/hooks/useViewer.d.ts +3 -2
  63. package/dist/hooks/useViewer.js +16 -18
  64. package/dist/hooks/useWildcardPack.d.ts +3 -1
  65. package/dist/hooks/useWildcardPack.js +19 -14
  66. package/dist/index.d.ts +30 -14
  67. package/dist/index.js +7 -7
  68. package/dist/internal/consentRefusalLatch.d.ts +1 -1
  69. package/dist/internal/consentRefusalLatch.js +1 -1
  70. package/dist/internal/liveHost.d.ts +63 -11
  71. package/dist/internal/liveHost.js +240 -37
  72. package/dist/internal/mockHost.js +16 -15
  73. package/dist/internal/pickerOverlay.d.ts +52 -4
  74. package/dist/internal/pickerOverlay.js +14 -3
  75. package/dist/testing.d.ts +1 -1
  76. package/dist/testing.js +1 -1
  77. package/dist/{internal → transport}/iframeTransport.d.ts +43 -5
  78. package/dist/{internal → transport}/iframeTransport.js +142 -12
  79. package/dist/transport/originMatcher.d.ts +92 -0
  80. package/dist/transport/originMatcher.js +222 -0
  81. package/dist/transport/requestId.d.ts +113 -0
  82. package/dist/transport/requestId.js +117 -0
  83. package/dist/{internal → transport}/validate.d.ts +34 -0
  84. package/dist/{internal → transport}/validate.js +189 -48
  85. package/dist/ui/BlockGate.js +1 -1
  86. package/dist/ui/SettingsForm.d.ts +8 -2
  87. package/dist/ui/SettingsForm.js +49 -3
  88. package/package.json +7 -148
  89. package/dist/api/generationResources.d.ts.map +0 -1
  90. package/dist/api/generationResources.js.map +0 -1
  91. package/dist/hooks/SfwGate.d.ts.map +0 -1
  92. package/dist/hooks/SfwGate.js.map +0 -1
  93. package/dist/hooks/useAppStorage.d.ts.map +0 -1
  94. package/dist/hooks/useAppStorage.js.map +0 -1
  95. package/dist/hooks/useAppWorkflows.d.ts.map +0 -1
  96. package/dist/hooks/useAppWorkflows.js.map +0 -1
  97. package/dist/hooks/useBlockAnalytics.d.ts.map +0 -1
  98. package/dist/hooks/useBlockAnalytics.js.map +0 -1
  99. package/dist/hooks/useBlockBreakpoint.d.ts.map +0 -1
  100. package/dist/hooks/useBlockBreakpoint.js.map +0 -1
  101. package/dist/hooks/useBlockContext.d.ts.map +0 -1
  102. package/dist/hooks/useBlockContext.js.map +0 -1
  103. package/dist/hooks/useBlockResize.d.ts.map +0 -1
  104. package/dist/hooks/useBlockResize.js.map +0 -1
  105. package/dist/hooks/useBlockSettings.d.ts.map +0 -1
  106. package/dist/hooks/useBlockSettings.js.map +0 -1
  107. package/dist/hooks/useBlockTheme.d.ts.map +0 -1
  108. package/dist/hooks/useBlockTheme.js.map +0 -1
  109. package/dist/hooks/useBlockToken.d.ts.map +0 -1
  110. package/dist/hooks/useBlockToken.js.map +0 -1
  111. package/dist/hooks/useBuzzAccounts.d.ts.map +0 -1
  112. package/dist/hooks/useBuzzAccounts.js.map +0 -1
  113. package/dist/hooks/useBuzzBalance.d.ts.map +0 -1
  114. package/dist/hooks/useBuzzBalance.js.map +0 -1
  115. package/dist/hooks/useBuzzPurchase.d.ts.map +0 -1
  116. package/dist/hooks/useBuzzPurchase.js.map +0 -1
  117. package/dist/hooks/useBuzzTransactions.d.ts.map +0 -1
  118. package/dist/hooks/useBuzzTransactions.js.map +0 -1
  119. package/dist/hooks/useBuzzWorkflow.d.ts.map +0 -1
  120. package/dist/hooks/useBuzzWorkflow.js.map +0 -1
  121. package/dist/hooks/useCheckpointPicker.d.ts.map +0 -1
  122. package/dist/hooks/useCheckpointPicker.js.map +0 -1
  123. package/dist/hooks/useCivitaiNavigate.d.ts.map +0 -1
  124. package/dist/hooks/useCivitaiNavigate.js.map +0 -1
  125. package/dist/hooks/useCollectionFollow.d.ts.map +0 -1
  126. package/dist/hooks/useCollectionFollow.js.map +0 -1
  127. package/dist/hooks/useConsentUnavailable.d.ts.map +0 -1
  128. package/dist/hooks/useConsentUnavailable.js.map +0 -1
  129. package/dist/hooks/useCreatePostFromApp.d.ts.map +0 -1
  130. package/dist/hooks/useCreatePostFromApp.js.map +0 -1
  131. package/dist/hooks/useDailyCompensation.d.ts.map +0 -1
  132. package/dist/hooks/useDailyCompensation.js.map +0 -1
  133. package/dist/hooks/useDirectLoad.d.ts.map +0 -1
  134. package/dist/hooks/useDirectLoad.js.map +0 -1
  135. package/dist/hooks/useDomainMaturity.d.ts.map +0 -1
  136. package/dist/hooks/useDomainMaturity.js.map +0 -1
  137. package/dist/hooks/useGatedImages.d.ts.map +0 -1
  138. package/dist/hooks/useGatedImages.js.map +0 -1
  139. package/dist/hooks/useGenerationResources.d.ts.map +0 -1
  140. package/dist/hooks/useGenerationResources.js.map +0 -1
  141. package/dist/hooks/useHostOrigin.d.ts.map +0 -1
  142. package/dist/hooks/useHostOrigin.js.map +0 -1
  143. package/dist/hooks/useImageUpload.d.ts.map +0 -1
  144. package/dist/hooks/useImageUpload.js.map +0 -1
  145. package/dist/hooks/usePublishGenerationOutputs.d.ts.map +0 -1
  146. package/dist/hooks/usePublishGenerationOutputs.js.map +0 -1
  147. package/dist/hooks/useRequestConsent.d.ts.map +0 -1
  148. package/dist/hooks/useRequestConsent.js.map +0 -1
  149. package/dist/hooks/useRequestSignIn.d.ts.map +0 -1
  150. package/dist/hooks/useRequestSignIn.js.map +0 -1
  151. package/dist/hooks/useResourcePicker.d.ts.map +0 -1
  152. package/dist/hooks/useResourcePicker.js.map +0 -1
  153. package/dist/hooks/useSaveImage.d.ts.map +0 -1
  154. package/dist/hooks/useSaveImage.js.map +0 -1
  155. package/dist/hooks/useSharedStorage.d.ts.map +0 -1
  156. package/dist/hooks/useSharedStorage.js.map +0 -1
  157. package/dist/hooks/useTip.d.ts.map +0 -1
  158. package/dist/hooks/useTip.js.map +0 -1
  159. package/dist/hooks/useTipAllowance.d.ts.map +0 -1
  160. package/dist/hooks/useTipAllowance.js.map +0 -1
  161. package/dist/hooks/useViewer.d.ts.map +0 -1
  162. package/dist/hooks/useViewer.js.map +0 -1
  163. package/dist/hooks/useWildcardPack.d.ts.map +0 -1
  164. package/dist/hooks/useWildcardPack.js.map +0 -1
  165. package/dist/index.d.ts.map +0 -1
  166. package/dist/index.js.map +0 -1
  167. package/dist/internal/catalog.d.ts.map +0 -1
  168. package/dist/internal/catalog.js.map +0 -1
  169. package/dist/internal/consent.d.ts.map +0 -1
  170. package/dist/internal/consent.js.map +0 -1
  171. package/dist/internal/consentRefusalLatch.d.ts.map +0 -1
  172. package/dist/internal/consentRefusalLatch.js.map +0 -1
  173. package/dist/internal/detector.d.ts.map +0 -1
  174. package/dist/internal/detector.js.map +0 -1
  175. package/dist/internal/directLoad.d.ts.map +0 -1
  176. package/dist/internal/directLoad.js.map +0 -1
  177. package/dist/internal/iframeTransport.d.ts.map +0 -1
  178. package/dist/internal/iframeTransport.js.map +0 -1
  179. package/dist/internal/inlineTransport.d.ts.map +0 -1
  180. package/dist/internal/inlineTransport.js.map +0 -1
  181. package/dist/internal/liveHost.d.ts.map +0 -1
  182. package/dist/internal/liveHost.js.map +0 -1
  183. package/dist/internal/mockHost.d.ts.map +0 -1
  184. package/dist/internal/mockHost.js.map +0 -1
  185. package/dist/internal/originMatcher.d.ts +0 -28
  186. package/dist/internal/originMatcher.d.ts.map +0 -1
  187. package/dist/internal/originMatcher.js +0 -89
  188. package/dist/internal/originMatcher.js.map +0 -1
  189. package/dist/internal/pickerOverlay.d.ts.map +0 -1
  190. package/dist/internal/pickerOverlay.js.map +0 -1
  191. package/dist/internal/replyError.d.ts.map +0 -1
  192. package/dist/internal/replyError.js.map +0 -1
  193. package/dist/internal/requestTimeouts.d.ts.map +0 -1
  194. package/dist/internal/requestTimeouts.js.map +0 -1
  195. package/dist/internal/singleton.d.ts.map +0 -1
  196. package/dist/internal/singleton.js.map +0 -1
  197. package/dist/internal/transport.d.ts.map +0 -1
  198. package/dist/internal/transport.js.map +0 -1
  199. package/dist/internal/validate.d.ts.map +0 -1
  200. package/dist/internal/validate.js.map +0 -1
  201. package/dist/live.d.ts.map +0 -1
  202. package/dist/live.js.map +0 -1
  203. package/dist/testing.d.ts.map +0 -1
  204. package/dist/testing.js.map +0 -1
  205. package/dist/ui/Alert.d.ts.map +0 -1
  206. package/dist/ui/Alert.js.map +0 -1
  207. package/dist/ui/Badge.d.ts.map +0 -1
  208. package/dist/ui/Badge.js.map +0 -1
  209. package/dist/ui/BlockGate.d.ts.map +0 -1
  210. package/dist/ui/BlockGate.js.map +0 -1
  211. package/dist/ui/Button.d.ts.map +0 -1
  212. package/dist/ui/Button.js.map +0 -1
  213. package/dist/ui/Card.d.ts.map +0 -1
  214. package/dist/ui/Card.js.map +0 -1
  215. package/dist/ui/Collapse.d.ts.map +0 -1
  216. package/dist/ui/Collapse.js.map +0 -1
  217. package/dist/ui/FollowButton.d.ts.map +0 -1
  218. package/dist/ui/FollowButton.js.map +0 -1
  219. package/dist/ui/Group.d.ts.map +0 -1
  220. package/dist/ui/Group.js.map +0 -1
  221. package/dist/ui/Loader.d.ts.map +0 -1
  222. package/dist/ui/Loader.js.map +0 -1
  223. package/dist/ui/Modal.d.ts.map +0 -1
  224. package/dist/ui/Modal.js.map +0 -1
  225. package/dist/ui/NumberInput.d.ts.map +0 -1
  226. package/dist/ui/NumberInput.js.map +0 -1
  227. package/dist/ui/ReportButton.d.ts.map +0 -1
  228. package/dist/ui/ReportButton.js.map +0 -1
  229. package/dist/ui/ResourceCard.d.ts.map +0 -1
  230. package/dist/ui/ResourceCard.js.map +0 -1
  231. package/dist/ui/SegmentedControl.d.ts.map +0 -1
  232. package/dist/ui/SegmentedControl.js.map +0 -1
  233. package/dist/ui/Select.d.ts.map +0 -1
  234. package/dist/ui/Select.js.map +0 -1
  235. package/dist/ui/SettingsForm.d.ts.map +0 -1
  236. package/dist/ui/SettingsForm.js.map +0 -1
  237. package/dist/ui/Slider.d.ts.map +0 -1
  238. package/dist/ui/Slider.js.map +0 -1
  239. package/dist/ui/Stack.d.ts.map +0 -1
  240. package/dist/ui/Stack.js.map +0 -1
  241. package/dist/ui/TextInput.d.ts.map +0 -1
  242. package/dist/ui/TextInput.js.map +0 -1
  243. package/dist/ui/Textarea.d.ts.map +0 -1
  244. package/dist/ui/Textarea.js.map +0 -1
  245. package/dist/ui/TipButton.d.ts.map +0 -1
  246. package/dist/ui/TipButton.js.map +0 -1
  247. package/dist/ui/index.d.ts.map +0 -1
  248. package/dist/ui/index.js.map +0 -1
  249. package/dist/ui/styles.d.ts.map +0 -1
  250. package/dist/ui/styles.js.map +0 -1
  251. /package/dist/{internal → transport}/detector.d.ts +0 -0
  252. /package/dist/{internal → transport}/detector.js +0 -0
  253. /package/dist/{internal → transport}/directLoad.d.ts +0 -0
  254. /package/dist/{internal → transport}/directLoad.js +0 -0
  255. /package/dist/{internal → transport}/inlineTransport.d.ts +0 -0
  256. /package/dist/{internal → transport}/inlineTransport.js +0 -0
  257. /package/dist/{internal → transport}/requestTimeouts.d.ts +0 -0
  258. /package/dist/{internal → transport}/requestTimeouts.js +0 -0
  259. /package/dist/{internal → transport}/singleton.d.ts +0 -0
  260. /package/dist/{internal → transport}/singleton.js +0 -0
  261. /package/dist/{internal → transport}/transport.d.ts +0 -0
  262. /package/dist/{internal → transport}/transport.js +0 -0
@@ -53,6 +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 { isRoutableRequestId, isWireRequestIdShape } from './requestId.js';
56
57
  const isObject = (v) => v !== null && typeof v === 'object';
57
58
  const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
58
59
  /** Accepts an ISO 8601 string that `new Date()` can parse to a finite timestamp. */
@@ -358,7 +359,7 @@ export function isValidTokenRefreshResponse(p) {
358
359
  return false;
359
360
  if (!isValidWrappedToken(p.token))
360
361
  return false;
361
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
362
+ if (!isWireRequestIdShape(p.requestId))
362
363
  return false;
363
364
  return true;
364
365
  }
@@ -367,7 +368,7 @@ export function isValidWorkflowReply(p) {
367
368
  return false;
368
369
  if (!isValidWorkflowSnapshot(p.snapshot))
369
370
  return false;
370
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
371
+ if (!isWireRequestIdShape(p.requestId))
371
372
  return false;
372
373
  return true;
373
374
  }
@@ -378,7 +379,7 @@ export function isValidBuzzPurchaseResult(p) {
378
379
  return false;
379
380
  if (p.newBalance !== undefined && typeof p.newBalance !== 'number')
380
381
  return false;
381
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
382
+ if (!isWireRequestIdShape(p.requestId))
382
383
  return false;
383
384
  return true;
384
385
  }
@@ -392,7 +393,7 @@ const isFiniteNumber = (v) => typeof v === 'number' && Number.isFinite(v);
392
393
  export function isValidBuzzBalanceResult(p) {
393
394
  if (!isObject(p))
394
395
  return false;
395
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
396
+ if (!isWireRequestIdShape(p.requestId))
396
397
  return false;
397
398
  if (p.error !== undefined && typeof p.error !== 'string')
398
399
  return false;
@@ -428,7 +429,7 @@ const VIEWER_STATUSES = new Set(['active', 'muted']);
428
429
  export function isValidViewerResult(p) {
429
430
  if (!isObject(p))
430
431
  return false;
431
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
432
+ if (!isWireRequestIdShape(p.requestId))
432
433
  return false;
433
434
  if (p.error !== undefined && typeof p.error !== 'string')
434
435
  return false;
@@ -513,7 +514,7 @@ function isPendingUpload(s) {
513
514
  export function isValidImageUploadResult(p) {
514
515
  if (!isObject(p))
515
516
  return false;
516
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
517
+ if (!isWireRequestIdShape(p.requestId))
517
518
  return false;
518
519
  // `selected` absent → cancelled upload (valid). When present, it must match
519
520
  // one of the purpose-keyed shapes (moderated / generationSource / pending).
@@ -541,7 +542,7 @@ export function isValidImageUploadResult(p) {
541
542
  export function isValidImageScanResolved(p) {
542
543
  if (!isObject(p))
543
544
  return false;
544
- if (!isNonEmptyString(p.requestId))
545
+ if (!isRoutableRequestId(p.requestId))
545
546
  return false;
546
547
  if (typeof p.imageId !== 'number' || !Number.isInteger(p.imageId) || p.imageId <= 0)
547
548
  return false;
@@ -569,7 +570,7 @@ export function isValidImageScanResolved(p) {
569
570
  export function isValidSharedUpdateResult(p) {
570
571
  if (!isObject(p))
571
572
  return false;
572
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
573
+ if (!isWireRequestIdShape(p.requestId))
573
574
  return false;
574
575
  if (p.error !== undefined && typeof p.error !== 'string')
575
576
  return false;
@@ -604,7 +605,7 @@ function isDateLike(v) {
604
605
  export function isValidBuzzTransactionsResult(p) {
605
606
  if (!isObject(p))
606
607
  return false;
607
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
608
+ if (!isWireRequestIdShape(p.requestId))
608
609
  return false;
609
610
  if (p.error !== undefined && typeof p.error !== 'string')
610
611
  return false;
@@ -644,7 +645,7 @@ export function isValidBuzzTransactionsResult(p) {
644
645
  export function isValidBuzzAccountsResult(p) {
645
646
  if (!isObject(p))
646
647
  return false;
647
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
648
+ if (!isWireRequestIdShape(p.requestId))
648
649
  return false;
649
650
  if (p.error !== undefined && typeof p.error !== 'string')
650
651
  return false;
@@ -675,7 +676,7 @@ export function isValidBuzzAccountsResult(p) {
675
676
  export function isValidDailyCompensationResult(p) {
676
677
  if (!isObject(p))
677
678
  return false;
678
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
679
+ if (!isWireRequestIdShape(p.requestId))
679
680
  return false;
680
681
  if (p.error !== undefined && typeof p.error !== 'string')
681
682
  return false;
@@ -711,7 +712,7 @@ const WILDCARD_PACK_ERROR_CODES = new Set([
711
712
  export function isValidWildcardPackResult(p) {
712
713
  if (!isObject(p))
713
714
  return false;
714
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
715
+ if (!isWireRequestIdShape(p.requestId))
715
716
  return false;
716
717
  if (p.error !== undefined) {
717
718
  if (typeof p.error !== 'string' || !WILDCARD_PACK_ERROR_CODES.has(p.error))
@@ -806,7 +807,7 @@ function isValidAppWorkflow(w) {
806
807
  export function isValidAppWorkflowsResult(p) {
807
808
  if (!isObject(p))
808
809
  return false;
809
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
810
+ if (!isWireRequestIdShape(p.requestId))
810
811
  return false;
811
812
  if (p.error !== undefined && typeof p.error !== 'string')
812
813
  return false;
@@ -835,7 +836,7 @@ export function isValidAppWorkflowsResult(p) {
835
836
  export function isValidCancelAppWorkflowResult(p) {
836
837
  if (!isObject(p))
837
838
  return false;
838
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
839
+ if (!isWireRequestIdShape(p.requestId))
839
840
  return false;
840
841
  if (p.error !== undefined && typeof p.error !== 'string')
841
842
  return false;
@@ -867,7 +868,7 @@ export function isValidCancelAppWorkflowResult(p) {
867
868
  export function isValidPublishResult(p) {
868
869
  if (!isObject(p))
869
870
  return false;
870
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
871
+ if (!isWireRequestIdShape(p.requestId))
871
872
  return false;
872
873
  if (p.error !== undefined && typeof p.error !== 'string')
873
874
  return false;
@@ -892,9 +893,18 @@ const GATED_IMAGE_STATUSES = new Set(['visible', 'hidden']);
892
893
  * `imageId` is always a finite number and `status` one of `visible`/`hidden`.
893
894
  * - `visible` → `url` a non-empty string and `width`/`height` each
894
895
  * `number | null`, PLUS exactly one of two rating shapes (below).
895
- * - `hidden` → ONLY `imageId` + `status`. A `hidden` entry carrying a `url` is
896
- * REJECTED — defense in depth against a buggy/hostile host leaking an
897
- * unclamped url on an image the viewer isn't allowed to see.
896
+ * - `hidden` → `imageId` + `status`, and a `url` on one is REJECTED — defense
897
+ * in depth against a buggy/hostile host leaking an unclamped url on an image
898
+ * the viewer isn't allowed to see.
899
+ *
900
+ * 🔴 THIS PREDICATE DOES **NOT** ENFORCE AN ALLOWLIST ON A `hidden` ENTRY, AND AN
901
+ * EARLIER VERSION OF THIS DOCBLOCK CLAIMED IT DID ("ONLY imageId + status").
902
+ * It bans exactly one SPELLING — the literal key `url`. `previewUrl`, `src`,
903
+ * `imageUrl` or any other key sails through here. The allowlist is real, but it
904
+ * lives one step later, in {@link projectGatedImage}: every `hidden` entry is
905
+ * narrowed to `{ imageId, status }` at the transport boundary, so no extra key
906
+ * can reach a consumer. See that function for why the split is PROJECTION for
907
+ * unknown keys and REJECTION for `url`.
898
908
  *
899
909
  * 🔴 THE TWO `visible` SHAPES ARE MUTUALLY EXCLUSIVE, AND THAT IS ENFORCED.
900
910
  * A RATED entry carries `nsfwLevel` + `contentRating` and NO `ratingPending`; an
@@ -946,23 +956,72 @@ function isValidGatedImage(img) {
946
956
  }
947
957
  return true;
948
958
  }
949
- // status === 'hidden': ONLY imageId + status. A leaked `url` on a hidden entry
950
- // is the exact cross-user moderation-boundary breach this guard exists to catch.
959
+ // status === 'hidden'. A leaked `url` is the exact cross-user moderation-
960
+ // boundary breach this guard exists to catch, and the contract forbids the key
961
+ // outright, so it is fatal here. Every OTHER key is left to
962
+ // `projectGatedImage`, which drops it — see that function's docblock for why
963
+ // the two cases are handled differently. This line is NOT an allowlist.
951
964
  if ('url' in img)
952
965
  return false;
953
966
  return true;
954
967
  }
968
+ /**
969
+ * Narrow ONE already-validated gated-image entry to the keys its `status`
970
+ * permits: a `hidden` entry becomes exactly `{ imageId, status }`; a `visible`
971
+ * entry is returned unchanged.
972
+ *
973
+ * 🔴 THIS IS WHERE THE `hidden` ALLOWLIST ACTUALLY LIVES. `isValidGatedImage`
974
+ * bans one spelling (`url`); this drops every key that is not `imageId` or
975
+ * `status`, so `previewUrl` / `src` / `imageUrl` / anything a future host adds
976
+ * cannot reach a consumer of `IMAGES_RESULT`.
977
+ *
978
+ * 🔴 WHY PROJECTION AND NOT REJECTION, for everything except `url`. A `false`
979
+ * from `isValidGatedImage` fails the WHOLE batch: `isValidImagesResult` returns
980
+ * false for one bad entry, `handleMessage` drops the entire `IMAGES_RESULT`
981
+ * before the pending-request lookup, and `useGatedImages().getImages()` then
982
+ * hangs to its transport timeout rather than rejecting. That failure mode is
983
+ * already documented in `useGatedImages.test.tsx` — it is what the
984
+ * `ratingPending` shape had to be admitted to avoid. The host is
985
+ * `civitai/civitai`, a separate repo on a separate release cadence, so an
986
+ * allowlist-by-rejection would convert ANY future host-side field addition into
987
+ * a block-wide hang. Dropping the key delivers the same moderation guarantee
988
+ * (nothing extra reaches a consumer) and costs a forward-compatible host
989
+ * nothing.
990
+ *
991
+ * `url` stays fatal because it is the one key the contract explicitly forbids on
992
+ * a `hidden` entry: a host can never legitimately add it, so it carries no
993
+ * forward-compat risk, and its presence is a live moderation breach worth
994
+ * surfacing loudly (`console.warn` + a `BLOCK_MESSAGE_REJECTED` report) rather
995
+ * than silently papering over.
996
+ *
997
+ * Identity-preserving: an entry that already satisfies the allowlist is returned
998
+ * as-is, so the common path allocates nothing.
999
+ */
1000
+ function projectGatedImage(img) {
1001
+ if (!isObject(img))
1002
+ return img;
1003
+ if (img.status !== 'hidden')
1004
+ return img;
1005
+ const keys = Object.keys(img);
1006
+ if (keys.length === 2 && 'imageId' in img && 'status' in img)
1007
+ return img;
1008
+ return { imageId: img.imageId, status: img.status };
1009
+ }
955
1010
  /**
956
1011
  * Reply to a block-initiated `GET_IMAGES_BY_IDS`. A well-formed reply carries
957
1012
  * EITHER a `result` ({ images: BlockGatedImage[] }) OR a free-text `error`; one
958
1013
  * with neither is malformed and dropped. Each image is shape-checked via
959
1014
  * {@link isValidGatedImage} — a `hidden` entry that carries a `url` DROPS the whole
960
1015
  * reply (defense in depth against a leaked unclamped url).
1016
+ *
1017
+ * Validation only. The `hidden` ALLOWLIST is applied separately, by
1018
+ * {@link projectInboundPayload} → {@link projectGatedImage}, after this returns
1019
+ * true.
961
1020
  */
962
1021
  export function isValidImagesResult(p) {
963
1022
  if (!isObject(p))
964
1023
  return false;
965
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1024
+ if (!isWireRequestIdShape(p.requestId))
966
1025
  return false;
967
1026
  if (p.error !== undefined && typeof p.error !== 'string')
968
1027
  return false;
@@ -1003,7 +1062,7 @@ export function isValidImagesResult(p) {
1003
1062
  export function isValidCollectionFollowResult(p) {
1004
1063
  if (!isObject(p))
1005
1064
  return false;
1006
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1065
+ if (!isWireRequestIdShape(p.requestId))
1007
1066
  return false;
1008
1067
  if (p.error !== undefined && typeof p.error !== 'string')
1009
1068
  return false;
@@ -1047,7 +1106,7 @@ export function isValidCollectionFollowResult(p) {
1047
1106
  export function isValidCreatePostResult(p) {
1048
1107
  if (!isObject(p))
1049
1108
  return false;
1050
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1109
+ if (!isWireRequestIdShape(p.requestId))
1051
1110
  return false;
1052
1111
  if (p.error !== undefined && typeof p.error !== 'string')
1053
1112
  return false;
@@ -1094,7 +1153,7 @@ export function isValidCreatePostResult(p) {
1094
1153
  export function isValidAppStorageGetResult(p) {
1095
1154
  if (!isObject(p))
1096
1155
  return false;
1097
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1156
+ if (!isWireRequestIdShape(p.requestId))
1098
1157
  return false;
1099
1158
  if (p.error !== undefined && typeof p.error !== 'string')
1100
1159
  return false;
@@ -1111,7 +1170,7 @@ export function isValidAppStorageGetResult(p) {
1111
1170
  export function isValidAppStorageSetResult(p) {
1112
1171
  if (!isObject(p))
1113
1172
  return false;
1114
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1173
+ if (!isWireRequestIdShape(p.requestId))
1115
1174
  return false;
1116
1175
  if (p.error !== undefined && typeof p.error !== 'string')
1117
1176
  return false;
@@ -1130,7 +1189,7 @@ export function isValidAppStorageSetResult(p) {
1130
1189
  export function isValidAppStorageDeleteResult(p) {
1131
1190
  if (!isObject(p))
1132
1191
  return false;
1133
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1192
+ if (!isWireRequestIdShape(p.requestId))
1134
1193
  return false;
1135
1194
  if (p.error !== undefined && typeof p.error !== 'string')
1136
1195
  return false;
@@ -1150,7 +1209,7 @@ export function isValidAppStorageDeleteResult(p) {
1150
1209
  export function isValidAppStorageListResult(p) {
1151
1210
  if (!isObject(p))
1152
1211
  return false;
1153
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1212
+ if (!isWireRequestIdShape(p.requestId))
1154
1213
  return false;
1155
1214
  if (p.error !== undefined && typeof p.error !== 'string')
1156
1215
  return false;
@@ -1174,7 +1233,7 @@ export function isValidAppStorageListResult(p) {
1174
1233
  export function isValidAppStorageQuotaResult(p) {
1175
1234
  if (!isObject(p))
1176
1235
  return false;
1177
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1236
+ if (!isWireRequestIdShape(p.requestId))
1178
1237
  return false;
1179
1238
  if (p.error !== undefined && typeof p.error !== 'string')
1180
1239
  return false;
@@ -1221,7 +1280,7 @@ function isValidSharedValue(v) {
1221
1280
  export function isValidSharedListResult(p) {
1222
1281
  if (!isObject(p))
1223
1282
  return false;
1224
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1283
+ if (!isWireRequestIdShape(p.requestId))
1225
1284
  return false;
1226
1285
  if (p.error !== undefined && typeof p.error !== 'string')
1227
1286
  return false;
@@ -1282,7 +1341,7 @@ function isValidSharedItemWire(it) {
1282
1341
  export function isValidSharedGetResult(p) {
1283
1342
  if (!isObject(p))
1284
1343
  return false;
1285
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1344
+ if (!isWireRequestIdShape(p.requestId))
1286
1345
  return false;
1287
1346
  if (p.error !== undefined && typeof p.error !== 'string')
1288
1347
  return false;
@@ -1301,7 +1360,7 @@ export function isValidSharedGetResult(p) {
1301
1360
  export function isValidSharedReportResult(p) {
1302
1361
  if (!isObject(p))
1303
1362
  return false;
1304
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1363
+ if (!isWireRequestIdShape(p.requestId))
1305
1364
  return false;
1306
1365
  if (p.error !== undefined && typeof p.error !== 'string')
1307
1366
  return false;
@@ -1320,7 +1379,7 @@ export function isValidSharedReportResult(p) {
1320
1379
  export function isValidSaveImageResult(p) {
1321
1380
  if (!isObject(p))
1322
1381
  return false;
1323
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1382
+ if (!isWireRequestIdShape(p.requestId))
1324
1383
  return false;
1325
1384
  if (p.error !== undefined && typeof p.error !== 'string')
1326
1385
  return false;
@@ -1338,7 +1397,7 @@ export function isValidSaveImageResult(p) {
1338
1397
  export function isValidSharedCountResult(p) {
1339
1398
  if (!isObject(p))
1340
1399
  return false;
1341
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1400
+ if (!isWireRequestIdShape(p.requestId))
1342
1401
  return false;
1343
1402
  if (p.error !== undefined && typeof p.error !== 'string')
1344
1403
  return false;
@@ -1352,7 +1411,7 @@ export function isValidSharedCountResult(p) {
1352
1411
  export function isValidSharedCountsResult(p) {
1353
1412
  if (!isObject(p))
1354
1413
  return false;
1355
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1414
+ if (!isWireRequestIdShape(p.requestId))
1356
1415
  return false;
1357
1416
  if (p.error !== undefined && typeof p.error !== 'string')
1358
1417
  return false;
@@ -1370,7 +1429,7 @@ export function isValidSharedCountsResult(p) {
1370
1429
  export function isValidSharedAppendResult(p) {
1371
1430
  if (!isObject(p))
1372
1431
  return false;
1373
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1432
+ if (!isWireRequestIdShape(p.requestId))
1374
1433
  return false;
1375
1434
  if (p.error !== undefined && typeof p.error !== 'string')
1376
1435
  return false;
@@ -1387,7 +1446,7 @@ export function isValidSharedAppendResult(p) {
1387
1446
  export function isValidSharedWithdrawResult(p) {
1388
1447
  if (!isObject(p))
1389
1448
  return false;
1390
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1449
+ if (!isWireRequestIdShape(p.requestId))
1391
1450
  return false;
1392
1451
  if (p.error !== undefined && typeof p.error !== 'string')
1393
1452
  return false;
@@ -1435,7 +1494,7 @@ function isValidPickerResourceBase(s) {
1435
1494
  export function isValidCheckpointPickerResult(p) {
1436
1495
  if (!isObject(p))
1437
1496
  return false;
1438
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1497
+ if (!isWireRequestIdShape(p.requestId))
1439
1498
  return false;
1440
1499
  if (p.selected !== undefined) {
1441
1500
  if (!isObject(p.selected))
@@ -1455,7 +1514,7 @@ export function isValidCheckpointPickerResult(p) {
1455
1514
  export function isValidResourcePickerResult(p) {
1456
1515
  if (!isObject(p))
1457
1516
  return false;
1458
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1517
+ if (!isWireRequestIdShape(p.requestId))
1459
1518
  return false;
1460
1519
  if (p.selected !== undefined) {
1461
1520
  const s = p.selected;
@@ -1494,7 +1553,7 @@ export function isValidResourcePickerResult(p) {
1494
1553
  export function isValidUserCheckpointSetResult(p) {
1495
1554
  if (!isObject(p))
1496
1555
  return false;
1497
- if (p.requestId !== undefined && typeof p.requestId !== 'string')
1556
+ if (!isWireRequestIdShape(p.requestId))
1498
1557
  return false;
1499
1558
  if (p.error !== undefined && typeof p.error !== 'string')
1500
1559
  return false;
@@ -1504,15 +1563,70 @@ export function isValidUserCheckpointSetResult(p) {
1504
1563
  return false;
1505
1564
  return true;
1506
1565
  }
1566
+ /**
1567
+ * Narrow an already-VALIDATED inbound payload to the fields a consumer is
1568
+ * allowed to see, immediately before `iframeTransport` hands it to a pending
1569
+ * request or a push listener.
1570
+ *
1571
+ * Validation answers "may this message be delivered at all"; projection answers
1572
+ * "which of its fields may cross". They are deliberately separate steps: a
1573
+ * predicate can only ever say yes/no about the WHOLE reply, so expressing an
1574
+ * allowlist as a predicate makes one unexpected key fatal to every entry in the
1575
+ * batch. See {@link projectGatedImage} for the concrete case and the hang it
1576
+ * avoids.
1577
+ *
1578
+ * 🔴 SCOPE, STATED HONESTLY: `IMAGES_RESULT` is the ONLY type projected today.
1579
+ * Every other type is returned by identity — this is not a general sanitizer and
1580
+ * must not be read as one. Add a case here (and say so in the type's validator
1581
+ * docblock) if another message ever needs one.
1582
+ */
1583
+ export function projectInboundPayload(type, payload) {
1584
+ if (type !== 'IMAGES_RESULT')
1585
+ return payload;
1586
+ if (!isObject(payload))
1587
+ return payload;
1588
+ const result = payload.result;
1589
+ if (!isObject(result) || !Array.isArray(result.images))
1590
+ return payload;
1591
+ let changed = false;
1592
+ const images = result.images.map((img) => {
1593
+ const projected = projectGatedImage(img);
1594
+ if (projected !== img)
1595
+ changed = true;
1596
+ return projected;
1597
+ });
1598
+ // Identity when nothing was dropped, so the overwhelmingly common path neither
1599
+ // allocates nor perturbs object identity for anything downstream.
1600
+ if (!changed)
1601
+ return payload;
1602
+ return { ...payload, result: { ...result, images } };
1603
+ }
1507
1604
  /**
1508
1605
  * Returns the validator for an inbound message type, or `null` for types
1509
1606
  * that don't carry a payload requiring shape checks (SUSPEND/RESUME).
1510
1607
  *
1511
1608
  * Falsy result from the validator means "drop the message"; `iframeTransport`
1512
1609
  * pairs that with a `console.warn` carrying the type name.
1610
+ *
1611
+ * 🔴 EXHAUSTIVE OVER `ParentToBlockMessage` AT COMPILE TIME. The `default:` arm
1612
+ * binds the switch subject to `never`, so a union member with no `case` above
1613
+ * fails `tsc` with `Type '"NEW_TYPE"' is not assignable to type 'never'`. Adding
1614
+ * a message type to the protocol and forgetting its validator is therefore a
1615
+ * BUILD error, not a silent unvalidated path.
1616
+ *
1617
+ * ⚠️ The gate proves the type is MAPPED, not that it is mapped to the RIGHT
1618
+ * validator, and not that it is mapped to a validator at all rather than to
1619
+ * `null` — `case 'NEW_TYPE': return null;` satisfies it. `validate.test.ts`
1620
+ * covers that residual by asserting a validator (not `null`) for every
1621
+ * payload-carrying type, plus identity for the highest-stakes few.
1513
1622
  */
1514
1623
  export function payloadValidatorFor(type) {
1515
- switch (type) {
1624
+ // Widened to `string` on the parameter so a JavaScript caller — and a block
1625
+ // running under a NEWER host than it was built against — reaches the
1626
+ // `default:` arm rather than bypassing the lookup. Narrowed here so the switch
1627
+ // has a union subject for the exhaustiveness check to bind.
1628
+ const messageType = type;
1629
+ switch (messageType) {
1516
1630
  case 'BLOCK_INIT':
1517
1631
  return isValidBlockInitPayload;
1518
1632
  case 'TOKEN_REFRESH':
@@ -1552,11 +1666,10 @@ export function payloadValidatorFor(type) {
1552
1666
  return isValidImagesResult;
1553
1667
  case 'COLLECTION_FOLLOW_RESULT':
1554
1668
  return isValidCollectionFollowResult;
1555
- // 🔴 THIS ENTRY IS NOT COMPILER-ENFORCED — the `default:` arm below returns
1556
- // `null`, which is a STRUCTURAL PASS. Omitting it would not fail the build
1557
- // and would not fail typecheck; it would ship an UNVALIDATED path, and
1558
- // there is a test (`payloadValidatorFor` mapping pin) that exists only
1559
- // because that failure is otherwise invisible.
1669
+ // 🔴 PRESENCE of this entry IS compiler-enforced (the `never` bind in
1670
+ // `default:`); WHICH validator it names is not. This reply settles a PUBLIC
1671
+ // POST, so the identity is pinned by test as well — see the mapping pin in
1672
+ // `validate.test.ts`.
1560
1673
  case 'CREATE_POST_RESULT':
1561
1674
  return isValidCreatePostResult;
1562
1675
  case 'IMAGE_UPLOAD_RESULT':
@@ -1602,10 +1715,38 @@ export function payloadValidatorFor(type) {
1602
1715
  case 'SUSPEND':
1603
1716
  case 'RESUME':
1604
1717
  return null;
1605
- default:
1606
- // Unknown type names get a structural pass; handleMessage's earlier
1607
- // `isMessage` branches won't match them anyway.
1718
+ default: {
1719
+ // ── COMPILE-TIME TOTALITY GATE ─────────────────────────────────────────
1720
+ // Every `ParentToBlockMessage` member is handled above, so the subject
1721
+ // narrows to `never` here. Add a member to the union without a `case` and
1722
+ // this assignment fails:
1723
+ // Type '"NEW_TYPE"' is not assignable to type 'never'.
1724
+ // Do NOT "fix" that by widening this binding — the error IS the gate.
1725
+ const exhaustive = messageType;
1726
+ void exhaustive;
1727
+ // ── RUNTIME: a type the union does not declare ─────────────────────────
1728
+ // 🔴 THIS ARM IS A STRUCTURAL PASS, AND THAT IS A DELIBERATE CHOICE
1729
+ // RATHER THAN AN OVERSIGHT — an earlier comment here justified it with
1730
+ // "handleMessage's earlier `isMessage` branches won't match them anyway",
1731
+ // which is true but is only a third of the reason and reads as if the
1732
+ // other paths had been checked. They have been, so state it: a type
1733
+ // outside the union cannot reach `pending.resolve` (`sendRequest`'s
1734
+ // `responseType` is keyed to the union), cannot reach a push listener
1735
+ // (`onMessage`'s `type` is too), and matches none of the three `isMessage`
1736
+ // branches. It is a no-op in `handleMessage` either way, so returning a
1737
+ // rejecting validator would protect nothing and would cost real
1738
+ // forward-compatibility: a host that has shipped a new message type ahead
1739
+ // of this package would make every older block in the fleet emit a
1740
+ // `console.warn` reading "dropping malformed …" (it is not malformed, it
1741
+ // is unrecognised) plus an unbudgeted `BLOCK_MESSAGE_REJECTED` beacon
1742
+ // labelled `other` on every delivery.
1743
+ //
1744
+ // The residual hole this leaves is narrow and worth naming: an UNTYPED
1745
+ // JavaScript consumer that registers `onMessage('SOME_FUTURE_TYPE', …)`
1746
+ // receives that payload unvalidated. Nothing in this package can validate
1747
+ // a type it does not know; such a consumer must validate for itself.
1608
1748
  return null;
1749
+ }
1609
1750
  }
1610
1751
  }
1611
1752
  //# sourceMappingURL=validate.js.map
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
- import { hostToRunUrl } from '../internal/directLoad.js';
3
+ import { hostToRunUrl } from '../transport/directLoad.js';
4
4
  import { useDirectLoad } from '../hooks/useDirectLoad.js';
5
5
  import { Card } from './Card.js';
6
6
  import { Stack } from './Stack.js';
@@ -30,8 +30,14 @@ export interface SettingsFormProps {
30
30
  /** Which slice of fields to render. */
31
31
  forScope: SettingScope;
32
32
  /**
33
- * Initial values keyed by field name. Missing keys fall back to the
34
- * manifest field's `default`. Unknown keys are ignored.
33
+ * Values keyed by field name. Missing keys fall back to the manifest
34
+ * field's `default`. Unknown keys are ignored.
35
+ *
36
+ * LIVE, not mount-only (#396): passing a different object after mount
37
+ * re-seeds every visible field the user has not yet edited, so values that
38
+ * arrive from an in-flight fetch land in the form instead of being dropped.
39
+ * Fields the user HAS edited keep the edit. The name is kept for API
40
+ * compatibility.
35
41
  */
36
42
  initialValues: Record<string, unknown>;
37
43
  /**
@@ -26,7 +26,30 @@ export function isFieldVisible(field, forScope, declaredScopes) {
26
26
  }
27
27
  export function SettingsForm(props) {
28
28
  const visibleFields = useMemo(() => Object.entries(props.manifestSettings).filter(([, def]) => isFieldVisible(def, props.forScope, props.declaredScopes)), [props.manifestSettings, props.forScope, props.declaredScopes]);
29
- const [values, setValues] = useState(() => {
29
+ /**
30
+ * The seed for the CURRENTLY visible fields: `initialValues` where present,
31
+ * otherwise the manifest `default`, otherwise `null`.
32
+ *
33
+ * 🔴 DERIVED, NOT SEEDED-ONCE STATE (#396). This used to be a `useState` lazy
34
+ * initializer, which runs exactly once — while `visibleFields` above is a
35
+ * `useMemo` that recomputes. The two drifted, in two ways that both reached
36
+ * the user:
37
+ *
38
+ * 1. `initialValues` that arrive ASYNCHRONOUSLY (the normal case: `{}`
39
+ * while a fetch is in flight, then the stored row) were ignored. The
40
+ * form rendered manifest defaults for ever, and Save wrote those
41
+ * defaults back OVER the user's stored value.
42
+ * 2. A `forScope` flip on a mounted form recomputed `visibleFields` to the
43
+ * new slice while `values` still held the OLD slice's keys — so the
44
+ * viewer-scope form rendered blank inputs and `onSubmit` posted the
45
+ * PUBLISHER slice's keys under a viewer-scope save.
46
+ *
47
+ * Recomputing instead of seeding closes both, and closes (2) STRUCTURALLY
48
+ * rather than by remembering to clear: `values` below is built by walking
49
+ * `seeded`'s keys, so a key outside the current `visibleFields` has no way
50
+ * into the submitted object at all.
51
+ */
52
+ const seeded = useMemo(() => {
30
53
  const seed = {};
31
54
  for (const [key, def] of visibleFields) {
32
55
  if (Object.prototype.hasOwnProperty.call(props.initialValues, key)) {
@@ -40,12 +63,35 @@ export function SettingsForm(props) {
40
63
  }
41
64
  }
42
65
  return seed;
43
- });
66
+ }, [visibleFields, props.initialValues]);
67
+ /**
68
+ * The user's own edits, keyed by field. Kept SEPARATE from the seed so that
69
+ * a later `initialValues`/`forScope` change re-seeds without discarding what
70
+ * the user typed, and so that an edit can never outlive its field's
71
+ * visibility (see `values`).
72
+ */
73
+ const [edits, setEdits] = useState({});
74
+ /**
75
+ * Seed overlaid with edits — the rendered AND submitted value set.
76
+ *
77
+ * 🔴 The iteration order is load-bearing: this walks `seeded`'s keys, NOT
78
+ * `edits`'. An edit to a field that is no longer visible (a `forScope` flip,
79
+ * or a manifest change) is therefore simply not present here, so it cannot
80
+ * be rendered and cannot be submitted. Spreading `edits` in wholesale would
81
+ * reintroduce exactly the cross-scope leak this fix exists to close.
82
+ */
83
+ const values = useMemo(() => {
84
+ const merged = {};
85
+ for (const key of Object.keys(seeded)) {
86
+ merged[key] = Object.prototype.hasOwnProperty.call(edits, key) ? edits[key] : seeded[key];
87
+ }
88
+ return merged;
89
+ }, [seeded, edits]);
44
90
  const [errors, setErrors] = useState({});
45
91
  const [submitting, setSubmitting] = useState(false);
46
92
  const [submitError, setSubmitError] = useState(null);
47
93
  const setFieldValue = useCallback((key, value) => {
48
- setValues((prev) => ({ ...prev, [key]: value }));
94
+ setEdits((prev) => ({ ...prev, [key]: value }));
49
95
  setErrors((prev) => {
50
96
  if (!prev[key])
51
97
  return prev;