@civitai/blocks-react 0.70.0 → 0.72.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.
@@ -334,6 +334,87 @@ export function isValidWorkflowSnapshot(s) {
334
334
  return false;
335
335
  if (s.trainingQuote !== undefined && !isValidTrainingQuote(s.trainingQuote))
336
336
  return false;
337
+ if (s.stepQuote !== undefined && !isValidStepQuote(s.stepQuote))
338
+ return false;
339
+ if (s.model3dOutputs !== undefined && !isValidModel3dOutputs(s.model3dOutputs))
340
+ return false;
341
+ if (s.steps !== undefined && !isValidWorkflowSteps(s.steps))
342
+ return false;
343
+ return true;
344
+ }
345
+ /**
346
+ * Shape check for `stepQuote` — the viewer-confirmable quote a pass-through
347
+ * `step` estimate returns when its fixed price is above the per-call budget. The
348
+ * block passes `quoteId` back verbatim, renders `total` on its confirm button
349
+ * and may show `stepType`, so all three are pinned to their types.
350
+ *
351
+ * `quoteId`'s `sq_…` FORMAT is deliberately not pinned HERE, for the reason
352
+ * given on {@link isValidTrainingQuote}: a host changing the handle's format
353
+ * must not turn every such estimate reply into a dropped one. The format is
354
+ * checked where it is an instruction rather than an observation — on the way
355
+ * OUT, in `useRunConfirmedStep()`.
356
+ */
357
+ function isValidStepQuote(q) {
358
+ if (!isObject(q))
359
+ return false;
360
+ if (!isNonEmptyString(q.quoteId))
361
+ return false;
362
+ if (!isFiniteNumber(q.total))
363
+ return false;
364
+ if (typeof q.stepType !== 'string')
365
+ return false;
366
+ return typeof q.expiresAt === 'string';
367
+ }
368
+ /**
369
+ * Shape check for `model3dOutputs` — shared by {@link isValidWorkflowSnapshot}
370
+ * and the `AppWorkflow` row guard, which carry the same field. A block matches
371
+ * an entry's `url` against its image list and loads it, and branches on
372
+ * `format` / `variant`, so each is pinned to a string. `format` and `variant`
373
+ * are OPEN sets (the orchestrator's own format names and output field paths)
374
+ * and are never checked against a list; `variant` may be the empty string.
375
+ */
376
+ function isValidModel3dOutputs(outputs) {
377
+ if (!Array.isArray(outputs))
378
+ return false;
379
+ for (const o of outputs) {
380
+ if (!isObject(o) || !isNonEmptyString(o.url))
381
+ return false;
382
+ if (typeof o.format !== 'string' || typeof o.variant !== 'string')
383
+ return false;
384
+ }
385
+ return true;
386
+ }
387
+ /**
388
+ * Shape check for `steps` — the per-step view of a `kind: 'workflow'` run.
389
+ * OPTIONAL like its neighbours, and a PRESENT but malformed value rejects the
390
+ * snapshot: the block reads `name`, `$type`, `status` and `imageUrls` off every
391
+ * entry.
392
+ *
393
+ * `status` is checked against the SAME closed set as the snapshot's own
394
+ * `status`, unlike the open string unions above: it is typed as that closed
395
+ * union and a block `switch`es on it, so a snapshot carrying any other value is
396
+ * dropped rather than handed over. `output` is `unknown` by contract and is not
397
+ * inspected.
398
+ */
399
+ function isValidWorkflowSteps(steps) {
400
+ if (!Array.isArray(steps))
401
+ return false;
402
+ for (const step of steps) {
403
+ if (!isObject(step))
404
+ return false;
405
+ if (typeof step.name !== 'string' || typeof step.$type !== 'string')
406
+ return false;
407
+ if (typeof step.status !== 'string' || !WORKFLOW_STATUSES.has(step.status))
408
+ return false;
409
+ if (step.imageUrls !== undefined) {
410
+ if (!Array.isArray(step.imageUrls))
411
+ return false;
412
+ if (!step.imageUrls.every((u) => typeof u === 'string'))
413
+ return false;
414
+ }
415
+ if (step.error !== undefined && typeof step.error !== 'string')
416
+ return false;
417
+ }
337
418
  return true;
338
419
  }
339
420
  /**
@@ -998,6 +1079,8 @@ function isValidAppWorkflow(w) {
998
1079
  return false;
999
1080
  if (w.trainedEpochs !== undefined && !isValidTrainedEpochs(w.trainedEpochs))
1000
1081
  return false;
1082
+ if (w.model3dOutputs !== undefined && !isValidModel3dOutputs(w.model3dOutputs))
1083
+ return false;
1001
1084
  return true;
1002
1085
  }
1003
1086
  /**
@@ -1400,6 +1483,31 @@ export function isValidTrainingResult(p) {
1400
1483
  return false;
1401
1484
  return true;
1402
1485
  }
1486
+ /**
1487
+ * Reply to `RUN_CONFIRMED_STEP`. A well-formed reply carries EITHER a `snapshot`
1488
+ * (a valid {@link isValidWorkflowSnapshot} — the block polls its `workflowId`)
1489
+ * OR an `error`; one with neither is malformed and dropped.
1490
+ *
1491
+ * 🔴 `error` IS SHAPE-CHECKED ONLY, for the reason given on
1492
+ * {@link isValidTrainingResult}: this request carries the same TEN-MINUTE
1493
+ * consent bound, and the host forwards server refusals (an expired or used
1494
+ * quote, a changed body or price, the feature flag) in the same field as its own
1495
+ * codes. A membership check would drop those and leave a Run button spinning for
1496
+ * ten minutes.
1497
+ */
1498
+ export function isValidConfirmedStepResult(p) {
1499
+ if (!isObject(p))
1500
+ return false;
1501
+ if (!isWireRequestIdShape(p.requestId))
1502
+ return false;
1503
+ if (p.error !== undefined && typeof p.error !== 'string')
1504
+ return false;
1505
+ if (p.snapshot !== undefined && !isValidWorkflowSnapshot(p.snapshot))
1506
+ return false;
1507
+ if (p.snapshot === undefined && p.error === undefined)
1508
+ return false;
1509
+ return true;
1510
+ }
1403
1511
  // ============================================================
1404
1512
  // App-storage (per-viewer KV) reply validators
1405
1513
  // ============================================================
@@ -2009,6 +2117,9 @@ export function payloadValidatorFor(type) {
2009
2117
  return isValidTrainingDatasetResult;
2010
2118
  case 'TRAINING_RESULT':
2011
2119
  return isValidTrainingResult;
2120
+ // Settles a charged job, like TRAINING_RESULT directly above.
2121
+ case 'CONFIRMED_STEP_RESULT':
2122
+ return isValidConfirmedStepResult;
2012
2123
  case 'IMAGE_UPLOAD_RESULT':
2013
2124
  return isValidImageUploadResult;
2014
2125
  case 'IMAGE_SCAN_RESOLVED':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/blocks-react",
3
- "version": "0.70.0",
3
+ "version": "0.72.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/theme": "^0.5.2",
41
- "@civitai/components": "^0.9.3"
40
+ "@civitai/components": "^0.9.3",
41
+ "@civitai/theme": "^0.5.2"
42
42
  },
43
43
  "comment-peerDependencies": "This floor is DERIVED, not chosen — the lowest published app-sdk that exports every symbol this package imports. Do not edit it without reading ./PEER_FLOOR.md (in-repo, unpublished) and tests/guards/blocks-react-peer-floor.test.mjs.",
44
44
  "peerDependencies": {
@@ -58,7 +58,7 @@
58
58
  "typescript": "^5.9.2",
59
59
  "vite": "^8.0.14",
60
60
  "vitest": "^4.1.11",
61
- "@civitai/app-sdk": "^0.65.1"
61
+ "@civitai/app-sdk": "^0.67.0"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public"