@civitai/app-sdk 0.65.1 → 0.67.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.
package/README.md CHANGED
@@ -140,7 +140,7 @@ interface BlockInitPayload {
140
140
  | `isModelSlotContext(ctx)` / `isPageSlotContext(ctx)` | Runtime narrowing for the `slotId`-discriminated `BlockContext` union. Real checks on a value that crossed a `postMessage` boundary — they verify every field they assert, not just `slotId`. |
141
141
  | `fetchNestedDocument(opts)` | Embed one of your OWN bundled documents as an iframe `srcdoc`, because a nested `<iframe src>` of your own content **cannot load** — see [Nested documents](#nested-documents) below. `NestedDocumentError` carries a `.code` of `'invalid-url' \| 'http-error' \| 'network-error'`. |
142
142
  | `isSignedIn(viewer)` | **The sign-in gate.** `isSignedIn(useBlockContext().viewer)` — do not open-code it as `viewer !== null` or `viewer?.signedIn === true`. Which of those is correct has already changed once with the host contract, and this is the one place it is decided. It reads neither `viewer.id` nor `viewer.username` (both `@deprecated`), so nothing written through it changes when those are removed. Need the identity rather than the presence? `useViewer()` — scope-gated and audited per call. |
143
- | types | `BlockManifestV1`, `ManifestSettings` (+ field types), `BlockContext`, `ModelSlotContext`, `BlockCheckpointInfo`, `ShowcaseImage`, `BlockToken`, `WrappedToken`, `BlockSettings`, `ViewerInfo`, `Theme`, `WorkflowBody`, `BlockTextToImageParams`, `WorkflowBodyCustomComfy` (+ its two arms `WorkflowBodyCustomComfyRecipe` / `WorkflowBodyCustomComfyInline`, and `InlineComfyNode`), `WorkflowBodyStep` / `WorkflowBodyPassThroughStep` (the two arms of `kind: 'step'`), `WorkflowBodyTraining` (+ `AiToolkitTrainingParams`, `BlockTrainingQuote`, `BlockTrainingDatasetItem`, `BlockTrainingDatasetResult` and the `BLOCK_TRAINING_*` bounds), `BlockWorkflowSnapshot`, `WorkflowStatus`, `BlockInitPayload`, `ParentToBlockMessage`, `BlockToParentMessage`. |
143
+ | types | `BlockManifestV1`, `ManifestSettings` (+ field types), `BlockContext`, `ModelSlotContext`, `BlockCheckpointInfo`, `ShowcaseImage`, `BlockToken`, `WrappedToken`, `BlockSettings`, `ViewerInfo`, `Theme`, `WorkflowBody`, `BlockTextToImageParams`, `WorkflowBodyCustomComfy` (+ its two arms `WorkflowBodyCustomComfyRecipe` / `WorkflowBodyCustomComfyInline`, and `InlineComfyNode`), `WorkflowBodyStep` / `WorkflowBodyPassThroughStep` (the two arms of `kind: 'step'`), `WorkflowBodyTraining` (+ `AiToolkitTrainingParams`, `BlockTrainingQuote`, `BlockTrainingDatasetItem`, `BlockTrainingDatasetResult` and the `BLOCK_TRAINING_*` bounds), `WorkflowBodyWorkflow` (+ `WorkflowBodyWorkflowStep`, `WorkflowStepRef`, `WithWorkflowStepRefs`, `BlockWorkflowStepSnapshot` and `BLOCK_WORKFLOW_MAX_STEPS`), `BlockStepQuote` / `BlockRunConfirmedStepHostError` (+ `BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB`, `BLOCK_STEP_QUOTE_ID_REGEX`), `BlockModel3dOutput`, `BlockWorkflowSnapshot`, `WorkflowStatus`, `BlockInitPayload`, `ParentToBlockMessage`, `BlockToParentMessage`. |
144
144
 
145
145
  `WorkflowBody`'s `customComfy` member is a discriminated union on `mode`, mirroring
146
146
  the host's `blockCustomComfyMemberSchema`. Narrow on the VALUE (`body.mode === 'inline'`),
@@ -209,6 +209,46 @@ const body: WorkflowBody = {
209
209
  };
210
210
  ```
211
211
 
212
+ **Above the per-call budget: a confirmed step.** A pass-through job the per-call budget refuses
213
+ can still run when the **viewer confirms its exact price** in Civitai's own dialog
214
+ (civitai/civitai#5786). When the orchestrator quotes a **fixed** price (positive, not variable
215
+ or metered) above the token's per-call budget and at most `BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB`
216
+ (5,000), the `estimate` reply carries `snapshot.stepQuote` (`quoteId`, `total`, `stepType`,
217
+ `expiresAt`). Send the same body back with that `quoteId` on the `RUN_CONFIRMED_STEP` bridge
218
+ (`CONFIRMED_STEP_RESULT`); a `quoteId` body sent as an ordinary submit is refused. A quote is
219
+ single-use and expires after 15 minutes. Only a **single** estimate carries `stepQuote` — a
220
+ batch estimate stores nothing, so its cells never do. Where a quote would be granted, the
221
+ estimate **fails instead** (`{ workflowId: 'failed', status: 'failed', error }`) for a training
222
+ `$type` in any casing (`a training step type cannot run as a confirmed step — use kind:'training'`) and for an
223
+ `input` that is not plain JSON (`a confirmed step needs plain JSON input (objects, arrays,
224
+ strings, finite numbers, booleans, null) — <path> is <what>`; the suffix varies, so match on
225
+ the text before the dash). Each confirmation is its own job, so a
226
+ `submission-unconfirmed` result — the outcome is unknown — must never be retried automatically:
227
+ a retry is a new, separately charged job. Page apps only, `ai:write:budgeted`, behind the host
228
+ flag `app-blocks-confirmed-step` (off by default), and refused for development, dev-tunnel and
229
+ review tokens — so it is exercised against the mock host, not `dev:live`. Use it through
230
+ `@civitai/blocks-react`'s `useRunConfirmedStep()`.
231
+
232
+ **3D outputs.** A step that produces 3D models (`polyGen`) still lists every model url in
233
+ `snapshot.imageUrls`; `snapshot.model3dOutputs` (and the same field on an `AppWorkflow` row)
234
+ labels them. Omitted when there are none.
235
+
236
+ ```ts
237
+ import type { BlockModel3dOutput, BlockWorkflowSnapshot } from '@civitai/app-sdk/blocks';
238
+
239
+ // `format` is the model's file format; `variant` is the output it came from.
240
+ function pickModel(
241
+ snapshot: BlockWorkflowSnapshot,
242
+ format: string,
243
+ variant: string,
244
+ ): BlockModel3dOutput | undefined {
245
+ return snapshot.model3dOutputs?.find((m) => m.format === format && m.variant === variant);
246
+ }
247
+
248
+ const mesh = pickModel(snapshot, 'glb', 'model');
249
+ const rigged = pickModel(snapshot, 'fbx', 'riggedFbxModel');
250
+ ```
251
+
212
252
  `WorkflowBodyTraining` (`kind: 'training'`) is the **quoted** way to train: an ai-toolkit LoRA
213
253
  run on a dataset Civitai prepared from the viewer's own images, mirroring the host's
214
254
  `blockTrainingBodySchema` (civitai/civitai#5434). There is no `maxBuzz` and no timeout — the
@@ -221,6 +261,71 @@ and review sessions. The dataset is prepared over `PREPARE_TRAINING_DATASET`
221
261
  (civitai/civitai#5438). Use it through `@civitai/blocks-react`'s `usePrepareTrainingDataset()`
222
262
  and `useRunTraining()`.
223
263
 
264
+ `WorkflowBodyWorkflow` (`kind: 'workflow'`) submits **several orchestrator steps as one
265
+ workflow**, 1 to `BLOCK_WORKFLOW_MAX_STEPS` (8), through the same messages as every other kind.
266
+ A value anywhere in a step's `input` may be a `WorkflowStepRef`
267
+ (`{ $ref, path, default?, format? }`) naming `'$arguments'`, an **earlier** step's `name`, or an
268
+ earlier **unnamed** step by position (`'$0'`). A `name` starts with a lowercase
269
+ letter, uses only lowercase letters, digits, `_` and `-`, and is unique (the exact rule is on
270
+ `WorkflowBodyWorkflowStep.name`). `suppressOutput: true` marks an intermediate step whose
271
+ blobs are not surfaced. There is no `maxBuzz`: the ceiling is the app's per-generation budget × the
272
+ number of steps.
273
+
274
+ ```ts
275
+ import type { WorkflowBody } from '@civitai/app-sdk/blocks';
276
+
277
+ const body: WorkflowBody = {
278
+ kind: 'workflow',
279
+ arguments: { prompt: 'a lighthouse at dusk' },
280
+ steps: [
281
+ {
282
+ name: 'draw',
283
+ $type: 'imageGen',
284
+ input: {
285
+ engine: 'google',
286
+ model: 'nano-banana-2',
287
+ prompt: { $ref: '$arguments', path: 'prompt' },
288
+ },
289
+ suppressOutput: true,
290
+ },
291
+ {
292
+ name: 'cutout',
293
+ $type: 'imageBackgroundRemoval',
294
+ input: { image: { $ref: 'draw', path: 'output.images[0].url' } },
295
+ },
296
+ ],
297
+ };
298
+ ```
299
+
300
+ - 🔴 **The estimate is not a maximum.** The orchestrator re-prices a step once the values it
301
+ references are known and charges the difference.
302
+ - **A workflow the host cannot price has no estimate, but its submit still proceeds.** The
303
+ estimate (and a batch-estimate cell) fails with no `cost` and `snapshot.errorCode`
304
+ `workflow_quote_rejected` (the orchestrator refused the body; the same body will fail again),
305
+ `workflow_quote_unavailable` (no usable answer; the one a retry can clear) or
306
+ `workflow_quote_unpriced` (an answer without a price). The submit of the same body is reserved
307
+ at the ceiling above — unless it contains a training step (refused without a quote on both, as
308
+ on the single-step arm) or a `repeat` step at any depth (also refused on both, before any
309
+ reservation, as `workflow_repeat_unbounded`; its `error` states the cause). An older host
310
+ returns the ceiling as the estimate's `cost.total` instead of failing.
311
+ - **One author fee per workflow**, on the quoted base, itemised as `cost.authorFee` on the
312
+ estimate. If the workflow bills more, the fee charged is held to the fee shown; a workflow that
313
+ does not succeed has its whole fee reversed, even if earlier steps delivered output.
314
+ - **`repeat` over `arguments` only, at most 16 iterations** (`BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS`,
315
+ nested repeats multiplied): `for.$ref` must be `'$arguments'` and `path` must name an array
316
+ there. A `merge` step's `fields` entries each need a string `$ref` (an earlier step or
317
+ `$arguments`) and a `reduce`. Repeat children appear in `snapshot.steps` as `<name>.<n>`. The
318
+ velocity limit counts one unit per step, and a `repeat` one per iteration.
319
+ - `snapshot.steps` reports each step's `status`, `imageUrls`, `output` and `error`. A `failed`
320
+ workflow can still carry `cost.total > 0` and usable `imageUrls`: steps that succeeded before
321
+ the failing one stay charged.
322
+ - A refused body carries `snapshot.errorCode`: `workflow_step_denied` (a platform-internal
323
+ `$type`), `workflow_ref_invalid` (a reference to the step itself, a later step, or nothing, or a
324
+ step name that breaks the naming rules, or a malformed `repeat`/`merge`),
325
+ `workflow_repeat_unbounded` (a `repeat` not over an array in `arguments`, or over 16 iterations,
326
+ or a `repeat` with no price quote) or `workflow_too_many_steps`. The single-step pass-through arm refuses `repeat` outright.
327
+ - Requires a host that knows the kind; an older host refuses the body.
328
+
224
329
  ### `defineBlock` validator rules
225
330
 
226
331
  **There is no rule list here, and that is the point.** `defineBlock` does not
@@ -109,7 +109,14 @@ export { isModelSlotContext, isPageSlotContext } from './types.js';
109
109
  * in `types.ts` for which of the two it uses and why.
110
110
  */
111
111
  export { isSignedIn } from './types.js';
112
- export type { BlockContext, KnownSlotId, ModelSlotId, PageSlotContext, PageSlotId, UnknownSlotContext, BlockAnalyticsDeclaration, BlockAnalyticsEventDeclaration, BlockAnalyticsPropertyDeclaration, BlockManifest, BlockManifestGood, BlockManifestV1, BlockSettings, BlockToken, ContentRating, ManifestBooleanField, ManifestIframe, ManifestNumberField, ManifestPage, ManifestPreview, ManifestSettingField, ManifestSettings, ManifestStringField, ManifestTarget, ModelSlotContext, SettingScope, SettingWidget, Theme, ViewerInfo, BlockCheckpointInfo, BlockResourceInfo, BlockResourcePickerType, BlockSourceImage, BlockUploadedImageInfo, BlockGenerationSourceImageInfo, BlockPendingImageInfo, BlockImageScanResult, BlockUploadPurpose, BlockNavigateScope, BlockTextToImageParams, BlockWorkflowSnapshot, BlockEstimateBatchAggregate, BlockModelSubstitution, ModelSubstitutionReason, KnownBlockWorkflowErrorCode, BlockStepToolCall, BlockTrainedEpoch, BlockPublishedModel, BuzzAccountType, ShowcaseImage, WorkflowBody, WorkflowBodyTextToImage, WorkflowBodyCustomComfy, WorkflowBodyCustomComfyRecipe, WorkflowBodyCustomComfyInline, InlineComfyNode, WorkflowBodyStep, WorkflowBodyPassThroughStep, WorkflowStatus, BlockBuzzTransaction, BlockBuzzAccount, BlockDailyCompensationResource, BlockViewer, BlockWildcardPack, BlockWildcardPackErrorCode, AppWorkflow, AppWorkflowImage, BlockGatedImage, BlockCollectionFollowErrorCode, BlockCollectionFollowResult, BlockPostSource, BlockCreatePostRequest, BlockCreatePostResult, BlockCreatePostHostError, WorkflowBodyTraining, AiToolkitTrainingParams, AiToolkitTrainingParamsBase, AiToolkitPlainEcosystem, AiToolkitLrScheduler, AiToolkitOptimizerType, BlockTrainingQuote, BlockTrainingDatasetItem, BlockTrainingDatasetResult, BlockTrainingRejectionReason, BlockPrepareTrainingDatasetHostError, BlockRunTrainingHostError, } from './types.js';
112
+ export type { BlockContext, KnownSlotId, ModelSlotId, PageSlotContext, PageSlotId, UnknownSlotContext, BlockAnalyticsDeclaration, BlockAnalyticsEventDeclaration, BlockAnalyticsPropertyDeclaration, BlockManifest, BlockManifestGood, BlockManifestV1, BlockSettings, BlockToken, ContentRating, ManifestBooleanField, ManifestIframe, ManifestNumberField, ManifestPage, ManifestPreview, ManifestSettingField, ManifestSettings, ManifestStringField, ManifestTarget, ModelSlotContext, SettingScope, SettingWidget, Theme, ViewerInfo, BlockCheckpointInfo, BlockResourceInfo, BlockResourcePickerType, BlockSourceImage, BlockUploadedImageInfo, BlockGenerationSourceImageInfo, BlockPendingImageInfo, BlockImageScanResult, BlockUploadPurpose, BlockNavigateScope, BlockTextToImageParams, BlockWorkflowSnapshot, BlockEstimateBatchAggregate, BlockModelSubstitution, ModelSubstitutionReason, KnownBlockWorkflowErrorCode, BlockStepToolCall, BlockTrainedEpoch, BlockPublishedModel, BuzzAccountType, ShowcaseImage, WorkflowBody, WorkflowBodyTextToImage, WorkflowBodyCustomComfy, WorkflowBodyCustomComfyRecipe, WorkflowBodyCustomComfyInline, InlineComfyNode, WorkflowBodyStep, WorkflowBodyPassThroughStep, WorkflowBodyWorkflow, WorkflowBodyWorkflowStep, WorkflowStepRef, WithWorkflowStepRefs, BlockWorkflowStepSnapshot, WorkflowStatus, BlockBuzzTransaction, BlockBuzzAccount, BlockDailyCompensationResource, BlockViewer, BlockWildcardPack, BlockWildcardPackErrorCode, AppWorkflow, AppWorkflowImage, BlockGatedImage, BlockCollectionFollowErrorCode, BlockCollectionFollowResult, BlockPostSource, BlockCreatePostRequest, BlockCreatePostResult, BlockCreatePostHostError, WorkflowBodyTraining, AiToolkitTrainingParams, AiToolkitTrainingParamsBase, AiToolkitPlainEcosystem, AiToolkitLrScheduler, AiToolkitOptimizerType, BlockTrainingQuote, BlockTrainingDatasetItem, BlockTrainingDatasetResult, BlockTrainingRejectionReason, BlockPrepareTrainingDatasetHostError, BlockRunTrainingHostError, BlockStepQuote, BlockModel3dOutput, BlockRunConfirmedStepHostError, } from './types.js';
113
+ /** Host bound for the App Blocks `kind: 'workflow'` body — see `WorkflowBodyWorkflow`. */
114
+ export { BLOCK_WORKFLOW_MAX_STEPS, BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS } from './types.js';
115
+ /**
116
+ * Host bounds for a viewer-confirmed pass-through step, mirrored from
117
+ * civitai/civitai — see `BlockWorkflowSnapshot.stepQuote`.
118
+ */
119
+ export { BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB, BLOCK_STEP_QUOTE_ID_REGEX } from './types.js';
113
120
  /**
114
121
  * Host bounds for the App Blocks `kind: 'training'` flow, mirrored from
115
122
  * civitai/civitai's zod — see `WorkflowBodyTraining`.
@@ -108,6 +108,13 @@ export { isModelSlotContext, isPageSlotContext } from './types.js';
108
108
  * in `types.ts` for which of the two it uses and why.
109
109
  */
110
110
  export { isSignedIn } from './types.js';
111
+ /** Host bound for the App Blocks `kind: 'workflow'` body — see `WorkflowBodyWorkflow`. */
112
+ export { BLOCK_WORKFLOW_MAX_STEPS, BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS } from './types.js';
113
+ /**
114
+ * Host bounds for a viewer-confirmed pass-through step, mirrored from
115
+ * civitai/civitai — see `BlockWorkflowSnapshot.stepQuote`.
116
+ */
117
+ export { BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB, BLOCK_STEP_QUOTE_ID_REGEX } from './types.js';
111
118
  /**
112
119
  * Host bounds for the App Blocks `kind: 'training'` flow, mirrored from
113
120
  * civitai/civitai's zod — see `WorkflowBodyTraining`.
@@ -8,7 +8,7 @@
8
8
  * Wire format: `window.postMessage({ type, payload }, targetOrigin)`.
9
9
  */
10
10
  import type { ColorDomain } from './browsingLevel.js';
11
- import type { BlockCheckpointInfo, BlockResourceInfo, BlockResourcePickerType, BlockUploadedImageInfo, BlockGenerationSourceImageInfo, BlockPendingImageInfo, BlockImageScanResult, BlockUploadPurpose, BlockNavigateScope, BlockContext, BlockSettings, Theme, ViewerInfo, WorkflowBody, BlockWorkflowSnapshot, BlockEstimateBatchAggregate, BlockBuzzTransaction, BlockBuzzAccount, BlockDailyCompensationResource, BlockViewer, BlockWildcardPack, BlockWildcardPackErrorCode, AppWorkflow, BlockGatedImage, BlockCollectionFollowErrorCode, BlockCollectionFollowResult, BlockPostSource, BlockCreatePostResult, BlockTrainingDatasetItem, BlockTrainingDatasetResult, WorkflowBodyTraining } from './types.js';
11
+ import type { BlockCheckpointInfo, BlockResourceInfo, BlockResourcePickerType, BlockUploadedImageInfo, BlockGenerationSourceImageInfo, BlockPendingImageInfo, BlockImageScanResult, BlockUploadPurpose, BlockNavigateScope, BlockContext, BlockSettings, Theme, ViewerInfo, WorkflowBody, BlockWorkflowSnapshot, BlockEstimateBatchAggregate, BlockBuzzTransaction, BlockBuzzAccount, BlockDailyCompensationResource, BlockViewer, BlockWildcardPack, BlockWildcardPackErrorCode, AppWorkflow, BlockGatedImage, BlockCollectionFollowErrorCode, BlockCollectionFollowResult, BlockPostSource, BlockCreatePostResult, BlockTrainingDatasetItem, BlockTrainingDatasetResult, WorkflowBodyTraining, WorkflowBodyPassThroughStep } from './types.js';
12
12
  /**
13
13
  * Filter params for `GET_BUZZ_TRANSACTIONS`. All optional; the host validates
14
14
  * them server-side (they are NEVER trusted for auth — the account is self-bound
@@ -508,6 +508,13 @@ export type ParentToBlockMessage = {
508
508
  snapshot?: BlockWorkflowSnapshot;
509
509
  error?: string;
510
510
  };
511
+ } | {
512
+ type: 'CONFIRMED_STEP_RESULT';
513
+ payload: {
514
+ requestId: string;
515
+ snapshot?: BlockWorkflowSnapshot;
516
+ error?: string;
517
+ };
511
518
  } | {
512
519
  type: 'IMAGES_RESULT';
513
520
  payload: {
@@ -846,6 +853,12 @@ export type BlockToParentMessage = {
846
853
  requestId: string;
847
854
  body: WorkflowBodyTraining;
848
855
  };
856
+ } | {
857
+ type: 'RUN_CONFIRMED_STEP';
858
+ payload: {
859
+ requestId: string;
860
+ body: WorkflowBodyPassThroughStep;
861
+ };
849
862
  } | {
850
863
  type: 'GET_IMAGES_BY_IDS';
851
864
  payload: {
@@ -1093,7 +1106,7 @@ export type BlockToParentMessageType = BlockToParentMessage['type'];
1093
1106
  * it is a measurement, not an invariant, and civitai's own compile-time gate
1094
1107
  * (published SDK union ⊆ `keyof INVENTORY`) is what holds one direction of it.
1095
1108
  */
1096
- export declare const BLOCK_TO_PARENT_MESSAGE_TYPES: readonly ["BLOCK_HELLO", "BLOCK_READY", "BLOCK_ERROR", "BLOCK_MESSAGE_REJECTED", "REQUEST_TOKEN", "RESIZE_IFRAME", "SUBMIT_WORKFLOW", "ESTIMATE_WORKFLOW", "ESTIMATE_WORKFLOW_BATCH", "POLL_WORKFLOW", "CANCEL_WORKFLOW", "OPEN_BUZZ_PURCHASE", "GET_BUZZ_BALANCE", "GET_VIEWER", "GET_BUZZ_TRANSACTIONS", "GET_BUZZ_ACCOUNTS", "GET_DAILY_COMPENSATION", "GET_WILDCARD_PACK", "QUERY_APP_WORKFLOWS", "PUBLISH_GENERATION_OUTPUTS", "CREATE_POST_FROM_APP", "PREPARE_TRAINING_DATASET", "RUN_TRAINING", "GET_IMAGES_BY_IDS", "CANCEL_APP_WORKFLOW", "OPEN_CHECKPOINT_PICKER", "OPEN_RESOURCE_PICKER", "OPEN_IMAGE_UPLOAD", "SET_USER_CHECKPOINT", "NAVIGATE", "REQUEST_SIGN_IN", "REQUEST_CONSENT", "TRACK_EVENT", "APP_STORAGE_GET", "APP_STORAGE_SET", "APP_STORAGE_DELETE", "APP_STORAGE_LIST", "APP_STORAGE_QUOTA", "SHARED_LIST", "SHARED_GET_COUNT", "SHARED_GET_COUNTS", "SHARED_APPEND", "SHARED_VOTE", "SHARED_UNVOTE", "SHARED_WITHDRAW", "SHARED_UPDATE", "SHARED_GET", "SHARED_REPORT", "SAVE_IMAGE", "SET_COLLECTION_FOLLOW"];
1109
+ export declare const BLOCK_TO_PARENT_MESSAGE_TYPES: readonly ["BLOCK_HELLO", "BLOCK_READY", "BLOCK_ERROR", "BLOCK_MESSAGE_REJECTED", "REQUEST_TOKEN", "RESIZE_IFRAME", "SUBMIT_WORKFLOW", "ESTIMATE_WORKFLOW", "ESTIMATE_WORKFLOW_BATCH", "POLL_WORKFLOW", "CANCEL_WORKFLOW", "OPEN_BUZZ_PURCHASE", "GET_BUZZ_BALANCE", "GET_VIEWER", "GET_BUZZ_TRANSACTIONS", "GET_BUZZ_ACCOUNTS", "GET_DAILY_COMPENSATION", "GET_WILDCARD_PACK", "QUERY_APP_WORKFLOWS", "PUBLISH_GENERATION_OUTPUTS", "CREATE_POST_FROM_APP", "PREPARE_TRAINING_DATASET", "RUN_TRAINING", "RUN_CONFIRMED_STEP", "GET_IMAGES_BY_IDS", "CANCEL_APP_WORKFLOW", "OPEN_CHECKPOINT_PICKER", "OPEN_RESOURCE_PICKER", "OPEN_IMAGE_UPLOAD", "SET_USER_CHECKPOINT", "NAVIGATE", "REQUEST_SIGN_IN", "REQUEST_CONSENT", "TRACK_EVENT", "APP_STORAGE_GET", "APP_STORAGE_SET", "APP_STORAGE_DELETE", "APP_STORAGE_LIST", "APP_STORAGE_QUOTA", "SHARED_LIST", "SHARED_GET_COUNT", "SHARED_GET_COUNTS", "SHARED_APPEND", "SHARED_VOTE", "SHARED_UNVOTE", "SHARED_WITHDRAW", "SHARED_UPDATE", "SHARED_GET", "SHARED_REPORT", "SAVE_IMAGE", "SET_COLLECTION_FOLLOW"];
1097
1110
  /**
1098
1111
  * The bucket a message type falls into when the protocol does not declare it.
1099
1112
  *
@@ -72,6 +72,7 @@ export const BLOCK_TO_PARENT_MESSAGE_TYPES = [
72
72
  'CREATE_POST_FROM_APP',
73
73
  'PREPARE_TRAINING_DATASET',
74
74
  'RUN_TRAINING',
75
+ 'RUN_CONFIRMED_STEP',
75
76
  'GET_IMAGES_BY_IDS',
76
77
  'CANCEL_APP_WORKFLOW',
77
78
  'OPEN_CHECKPOINT_PICKER',
@@ -1168,6 +1168,9 @@ export type WorkflowBodyStep = {
1168
1168
  * checkpoint itself.
1169
1169
  * - `$type` is bounded to **1…64 characters** and `input` to **262144 bytes**
1170
1170
  * (256 KB) serialized. Both REJECT; neither truncates.
1171
+ * - **`repeat` is refused on this arm**, outright: a `repeat` may only
1172
+ * iterate an array in a body's own `arguments`, and this arm has none. Use
1173
+ * a `kind: 'workflow'` body ({@link WorkflowBodyWorkflow}) to repeat a step.
1171
1174
  * - `maxBuzz` is the app's spend knob, and the host's quote can raise the
1172
1175
  * reservation above it — see its own note below.
1173
1176
  *
@@ -1248,6 +1251,237 @@ export type WorkflowBodyPassThroughStep = {
1248
1251
  * viewer as a maximum.
1249
1252
  */
1250
1253
  maxBuzz: number;
1254
+ /**
1255
+ * The viewer-confirmable quote this job is charged against —
1256
+ * {@link BlockStepQuote.quoteId} from this body's own estimate
1257
+ * (`sq_` + 32 lowercase hex characters, {@link BLOCK_STEP_QUOTE_ID_REGEX}).
1258
+ *
1259
+ * OMIT it everywhere except a CONFIRMED run: an estimate never carries it,
1260
+ * and neither does an ordinary submit. It is REQUIRED on
1261
+ * `RUN_CONFIRMED_STEP` (`useRunConfirmedStep()` in `@civitai/blocks-react`),
1262
+ * the only message that can run a job priced above the token's per-call
1263
+ * budget — see {@link BlockWorkflowSnapshot.stepQuote} for the flow.
1264
+ *
1265
+ * 🔴 A body carrying it is NOT runnable through `SUBMIT_WORKFLOW`. The server
1266
+ * charges a confirmed job only against a quote the viewer confirmed in
1267
+ * Civitai's own dialog from a signed-in session, so the same body sent as a
1268
+ * plain submit is refused. Change nothing else in the body between the
1269
+ * estimate and the run: the server checks it against the quoted one
1270
+ * (`quoteId` itself is excluded from that comparison).
1271
+ */
1272
+ quoteId?: string;
1273
+ };
1274
+ /**
1275
+ * Ceiling on what ONE viewer-confirmed pass-through job may cost, in Buzz. Host
1276
+ * constant `BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB`. A fixed price above it gets
1277
+ * no {@link BlockWorkflowSnapshot.stepQuote} and cannot be run from an app.
1278
+ */
1279
+ export declare const BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB = 5000;
1280
+ /**
1281
+ * The shape of a confirmed-step quote id ({@link BlockStepQuote.quoteId}): `sq_`
1282
+ * followed by 32 lowercase hex characters. Host constant
1283
+ * `BLOCK_STEP_QUOTE_ID_REGEX`. Disjoint from the training quote's `tq_` shape,
1284
+ * so neither kind's id can name the other's quote.
1285
+ */
1286
+ export declare const BLOCK_STEP_QUOTE_ID_REGEX: RegExp;
1287
+ /**
1288
+ * The most steps one `kind: 'workflow'` body may carry. The host refuses a
1289
+ * longer list with `errorCode: 'workflow_too_many_steps'`; an empty list is
1290
+ * refused too.
1291
+ */
1292
+ export declare const BLOCK_WORKFLOW_MAX_STEPS = 8;
1293
+ /**
1294
+ * The most times one top-level `repeat` step of a `kind: 'workflow'` body may
1295
+ * run its template, NESTED REPEATS MULTIPLIED (a 4-item repeat inside a 4-item
1296
+ * repeat is 16). The host refuses more with
1297
+ * `errorCode: 'workflow_repeat_unbounded'`.
1298
+ */
1299
+ export declare const BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS = 16;
1300
+ /**
1301
+ * A REFERENCE inside a `kind: 'workflow'` step's `input` — the orchestrator's
1302
+ * own mechanism for feeding one step's result into a later step. Put it
1303
+ * anywhere a value would go, at any depth.
1304
+ *
1305
+ * `$ref` names where the value comes from:
1306
+ * - `'$arguments'` — the body's own {@link WorkflowBodyWorkflow.arguments};
1307
+ * - the `name` of an EARLIER step in the same body;
1308
+ * - `'$<index>'` — an EARLIER step you did NOT name, by its zero-based
1309
+ * position (`'$0'` is the first step). A step that has a `name` is
1310
+ * referenced by that name only.
1311
+ *
1312
+ * An object counts as a reference only when it has BOTH `$ref` and `path` and
1313
+ * no key other than `default` / `format`; anything else is ordinary input.
1314
+ *
1315
+ * `path` selects the value inside that source, e.g. `'output.images[0].url'`
1316
+ * for a step, or `'prompt'` for `$arguments`. The orchestrator resolves it; the
1317
+ * path is not checked by this package or by the host.
1318
+ *
1319
+ * 🔴 A reference may only point BACKWARDS. A step that names itself, a later
1320
+ * step, or a step that does not exist is refused by the host with
1321
+ * `errorCode: 'workflow_ref_invalid'` before anything is priced or reserved.
1322
+ * A body may hold at most 64 references, in inputs nested at most 32 levels.
1323
+ *
1324
+ * `default` is the value used when the path resolves to nothing; `format` is a
1325
+ * template applied to the resolved value. Both are passed to the orchestrator
1326
+ * unmodified.
1327
+ */
1328
+ export type WorkflowStepRef = {
1329
+ $ref: '$arguments' | `$${number}` | (string & {});
1330
+ path: string;
1331
+ default?: unknown;
1332
+ format?: string;
1333
+ };
1334
+ /**
1335
+ * `T` with a {@link WorkflowStepRef} allowed in place of any value, at any
1336
+ * depth. Use it to keep a step input typed while still wiring references in:
1337
+ *
1338
+ * ```ts
1339
+ * type CutoutInput = { image: string; format?: 'png' | 'webp' };
1340
+ * const input: WithWorkflowStepRefs<CutoutInput> = {
1341
+ * image: { $ref: 'draw', path: 'output.images[0].url' },
1342
+ * format: 'png',
1343
+ * };
1344
+ * ```
1345
+ */
1346
+ export type WithWorkflowStepRefs<T> = WorkflowStepRef | (T extends ReadonlyArray<infer U> ? Array<WithWorkflowStepRefs<U>> : T extends object ? {
1347
+ [K in keyof T]: WithWorkflowStepRefs<T[K]>;
1348
+ } : T);
1349
+ /** One step of a {@link WorkflowBodyWorkflow}. */
1350
+ export type WorkflowBodyWorkflowStep = {
1351
+ /**
1352
+ * What later steps call this one in a {@link WorkflowStepRef}, and the `name`
1353
+ * it reports under on {@link BlockWorkflowSnapshot.steps}. Optional: an
1354
+ * unnamed step is named by its position (`'$0'`, `'$1'`, …).
1355
+ *
1356
+ * A lowercase letter followed by up to 31 of `a-z`, `0-9`, `_` or `-`
1357
+ * (`/^[a-z][a-z0-9_-]{0,31}$/`), unique within the body, and not
1358
+ * `'block-step'`, which is reserved. A name that breaks a rule is refused
1359
+ * with `errorCode: 'workflow_ref_invalid'`.
1360
+ */
1361
+ name?: string;
1362
+ /**
1363
+ * The ORCHESTRATOR step type to run, verbatim — e.g. `'imageGen'`. Not
1364
+ * resolved against any allowlist: it is refused only when it names a
1365
+ * platform-internal type (scanners, moderation classifiers, hashing, web
1366
+ * scrape/search, and the retired `textToImage` step type), with
1367
+ * `errorCode: 'workflow_step_denied'`. 1…64 characters.
1368
+ *
1369
+ * Two step types are read by shape, because they name their sources outside
1370
+ * a {@link WorkflowStepRef}:
1371
+ * - **`repeat` — over `arguments` only, at most
1372
+ * {@link BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS} iterations.** Its input must
1373
+ * be exactly `{ for: { $ref, path, as }, template: { $type, name?, input? } }`
1374
+ * with `for.$ref === '$arguments'` and `path` naming an ARRAY in the body's
1375
+ * `arguments`; its length is the iteration count, nested repeats
1376
+ * multiplied. A repeat over another step's output, a missing or non-array
1377
+ * path, or too many iterations is refused with
1378
+ * `errorCode: 'workflow_repeat_unbounded'`; a wrong shape, or repeats nested
1379
+ * more than 4 deep, with `workflow_ref_invalid`. Inside the template the
1380
+ * `as` name is a source a reference may name; it may not equal a step name.
1381
+ * The template's `$type` is held to the denylist too.
1382
+ * - **`merge`** — every entry of `input.fields` needs a string `$ref` and a
1383
+ * `reduce`, and each `$ref` must name `$arguments` or an EARLIER step,
1384
+ * never a repeat variable. Each counts toward the 64-reference cap.
1385
+ * Otherwise `workflow_ref_invalid`.
1386
+ */
1387
+ $type: string;
1388
+ /**
1389
+ * The step's own input object. Any value in it, at any depth, may be a
1390
+ * {@link WorkflowStepRef}. The orchestrator's per-`$type` schema is the only
1391
+ * authority for what a given `$type` accepts. Every step's `input` plus the
1392
+ * body's `arguments` may total at most 262144 bytes (256 KB) serialized.
1393
+ */
1394
+ input: Record<string, unknown>;
1395
+ /**
1396
+ * `true` marks an INTERMEDIATE step: its blobs are not surfaced, neither on
1397
+ * its own {@link BlockWorkflowStepSnapshot.imageUrls} nor in the top-level
1398
+ * {@link BlockWorkflowSnapshot.imageUrls}. The step still runs and is still
1399
+ * charged.
1400
+ */
1401
+ suppressOutput?: boolean;
1402
+ };
1403
+ /**
1404
+ * The `workflow` member of {@link WorkflowBody} — SEVERAL orchestrator steps
1405
+ * submitted as ONE workflow, where a later step can consume an earlier step's
1406
+ * result through a {@link WorkflowStepRef}. It travels through the same
1407
+ * messages and hooks as every other kind (`estimate`, `submit`, `watch`,
1408
+ * `cancel`, a batch-estimate cell, `useWorkflowBatch`).
1409
+ *
1410
+ * Between 1 and {@link BLOCK_WORKFLOW_MAX_STEPS} steps. There is no `maxBuzz`:
1411
+ * the spend ceiling is the app's per-generation budget × the number of steps,
1412
+ * enforced by the host. The host's schema is strict: an unknown key on the
1413
+ * body or on a step is rejected, not dropped.
1414
+ *
1415
+ * 🔴 THREE THINGS ABOUT THE PRICE, all of them different from a single-step
1416
+ * body:
1417
+ * 1. **The estimate is NOT a maximum.** When a step's input references an
1418
+ * earlier step, the orchestrator re-prices that step once the referenced
1419
+ * values are known and charges the difference. Show the estimate as an
1420
+ * estimate; never as "you will pay at most".
1421
+ * 2. **A workflow the host cannot price has no estimate, but its submit still
1422
+ * proceeds.** `estimate()` (and that body's cell in a batch estimate) FAILS
1423
+ * with no `cost` and `errorCode` `'workflow_quote_rejected'`,
1424
+ * `'workflow_quote_unavailable'` or `'workflow_quote_unpriced'` — see
1425
+ * {@link BlockWorkflowSnapshot.errorCode} for which of them a retry can
1426
+ * clear. `submit()` of the same body is NOT refused: it is reserved at the
1427
+ * ceiling above instead of at a quote. So a failed estimate does not predict
1428
+ * a refused submit. Two exceptions are refused on BOTH, before any
1429
+ * reservation: a workflow containing a training step, exactly as on the
1430
+ * single-step arm (a `training-quote-*` `errorCode`), and one with a
1431
+ * `repeat` step at any depth (`errorCode: 'workflow_repeat_unbounded'`).
1432
+ * Their `error` states the cause; only "none could be obtained; try again"
1433
+ * is one a retry of the same body can clear.
1434
+ * (An OLDER host returns the ceiling as the estimate's `cost.total` instead
1435
+ * of failing, so there a total equal to the ceiling may be unpriced.)
1436
+ * 3. **One author fee per workflow**, not one per step, computed on the
1437
+ * QUOTED base. The estimate's `cost.authorFee` is that single fee, already
1438
+ * inside `cost.total`. If the workflow bills more than the quote, the fee
1439
+ * charged is held to the fee shown. A workflow that does not succeed has
1440
+ * its WHOLE fee reversed, even when earlier steps delivered output.
1441
+ *
1442
+ * The app's velocity limit counts one unit per step, and a `repeat` step one
1443
+ * per iteration (nested repeats multiplied) instead of one.
1444
+ *
1445
+ * 🔴 A FAILED WORKFLOW CAN STILL HAVE COST AND IMAGES. Steps that succeeded
1446
+ * before the failing one stay charged, so a `failed` snapshot can carry
1447
+ * `cost.total > 0` and usable `imageUrls`. See {@link BlockWorkflowSnapshot.steps}.
1448
+ *
1449
+ * 🔴 REQUIRES A HOST THAT KNOWS THIS KIND. An older host refuses the body.
1450
+ *
1451
+ * @example An image step whose output a second step consumes.
1452
+ * ```ts
1453
+ * const body: WorkflowBody = {
1454
+ * kind: 'workflow',
1455
+ * arguments: { prompt: 'a lighthouse at dusk' },
1456
+ * steps: [
1457
+ * {
1458
+ * name: 'draw',
1459
+ * $type: 'imageGen',
1460
+ * input: {
1461
+ * engine: 'google',
1462
+ * model: 'nano-banana-2',
1463
+ * prompt: { $ref: '$arguments', path: 'prompt' },
1464
+ * },
1465
+ * suppressOutput: true,
1466
+ * },
1467
+ * {
1468
+ * name: 'cutout',
1469
+ * $type: 'imageBackgroundRemoval',
1470
+ * input: { image: { $ref: 'draw', path: 'output.images[0].url' } },
1471
+ * },
1472
+ * ],
1473
+ * };
1474
+ * ```
1475
+ */
1476
+ export type WorkflowBodyWorkflow = {
1477
+ kind: 'workflow';
1478
+ /** The steps, in order. 1…{@link BLOCK_WORKFLOW_MAX_STEPS}. */
1479
+ steps: WorkflowBodyWorkflowStep[];
1480
+ /**
1481
+ * Values the steps can read through `{ $ref: '$arguments', path }` — one
1482
+ * place to put a prompt or a seed that several steps share.
1483
+ */
1484
+ arguments?: Record<string, unknown>;
1251
1485
  };
1252
1486
  /** Ceiling on what ONE confirmed `kind: 'training'` run may cost, in Buzz. Host constant `BLOCK_TRAINING_MAX_BUZZ_PER_RUN`. */
1253
1487
  export declare const BLOCK_TRAINING_MAX_BUZZ_PER_RUN = 5000;
@@ -1431,6 +1665,10 @@ export interface WorkflowBodyTraining {
1431
1665
  * on a server-prepared dataset of the viewer's own images. ESTIMATE ONLY
1432
1666
  * through `useBuzzWorkflow`; it runs through `useRunTraining()`, whose
1433
1667
  * host-chrome consent dialog is the only way to confirm its quote.
1668
+ * - {@link WorkflowBodyWorkflow} (`kind: 'workflow'`) — up to
1669
+ * {@link BLOCK_WORKFLOW_MAX_STEPS} orchestrator steps submitted as one
1670
+ * workflow, a later step consuming an earlier one's result through a
1671
+ * {@link WorkflowStepRef}. Its estimate is not a maximum — read its doc.
1434
1672
  *
1435
1673
  * `kind: 'step'` is therefore itself a union, discriminated on the PRESENCE of
1436
1674
  * `step` — the same nesting {@link WorkflowBodyCustomComfy} has on `mode`.
@@ -1447,7 +1685,7 @@ export interface WorkflowBodyTraining {
1447
1685
  * over `kind`. Host code that must handle every member gets a compile error
1448
1686
  * pointing at the new one, which is the intended behaviour.
1449
1687
  */
1450
- export type WorkflowBody = WorkflowBodyTextToImage | WorkflowBodyCustomComfy | WorkflowBodyStep | WorkflowBodyPassThroughStep | WorkflowBodyTraining;
1688
+ export type WorkflowBody = WorkflowBodyTextToImage | WorkflowBodyCustomComfy | WorkflowBodyStep | WorkflowBodyPassThroughStep | WorkflowBodyTraining | WorkflowBodyWorkflow;
1451
1689
  /**
1452
1690
  * The host-mediated view of an orchestrator workflow that an iframe block
1453
1691
  * receives over `postMessage`.
@@ -1487,6 +1725,9 @@ export interface BlockWorkflowSnapshot {
1487
1725
  * orchestrator price quote, which refuses before any fee is looked up; and
1488
1726
  * the kinds that price no fee (`customComfy`, pass-through `step`,
1489
1727
  * `training`). **Do not read absent as `0`** — show `total` on its own.
1728
+ *
1729
+ * A `kind: 'workflow'` estimate itemises it too, and it is ONE fee for the
1730
+ * whole workflow however many steps it has.
1490
1731
  */
1491
1732
  authorFee?: number;
1492
1733
  };
@@ -1500,13 +1741,21 @@ export interface BlockWorkflowSnapshot {
1500
1741
  *
1501
1742
  * WHERE IT APPEARS today: the refusal for a pass-through `step` body
1502
1743
  * ({@link WorkflowBodyPassThroughStep}) whose `$type` is a training type
1503
- * (`'training'` or `'imageResourceTraining'`) when the host could not get a
1744
+ * (`'training'` or `'imageResourceTraining'`, in any casing — the host
1745
+ * compares `$type` case-insensitively) when the host could not get a
1504
1746
  * price quote for it. That refusal is `{ workflowId: 'failed', status:
1505
1747
  * 'failed', error, errorCode }` with no `cost`, and it is the reply to
1506
1748
  * `estimate()` (which rejects with `WorkflowEstimateError`), to `submit()`
1507
1749
  * (which rejects with `WorkflowSubmitError`, before anything is reserved or
1508
1750
  * charged), and to that body's cell in a batch estimate. Read it from the
1509
- * error's `.snapshot.errorCode`. The `kind: 'training'` arm does NOT set it.
1751
+ * error's `.snapshot.errorCode`. A `kind: 'training'` ESTIMATE does not set
1752
+ * it. The confirmed SUBMIT of a training run or a confirmed step can — see
1753
+ * `'daily-limit-exceeded'` below.
1754
+ * A refused `kind: 'workflow'` body ({@link WorkflowBodyWorkflow}) sets it in
1755
+ * the same shape, on the same three replies — except the three
1756
+ * `workflow_quote_*` codes, which are ESTIMATE-ONLY (`estimate()` and a
1757
+ * batch-estimate cell): `submit()` of the same body defines no such refusal
1758
+ * and proceeds, reserved at the ceiling.
1510
1759
  *
1511
1760
  * The values the host sends today ({@link KnownBlockWorkflowErrorCode}):
1512
1761
  * - `'training-quote-rejected'` — the orchestrator refused the request
@@ -1515,6 +1764,50 @@ export interface BlockWorkflowSnapshot {
1515
1764
  * - `'training-quote-unavailable'` — no usable answer (an outage, a timeout,
1516
1765
  * a rate limit, insufficient funds, …). Retrying later may succeed.
1517
1766
  * - `'training-quote-unpriced'` — the orchestrator answered without a price.
1767
+ * - `'daily-limit-exceeded'` — a viewer-confirmed job (a `kind: 'training'`
1768
+ * run or a confirmed step) would exceed what remains today of the daily
1769
+ * Buzz limit THE VIEWER set for this app. Nothing was charged and no job
1770
+ * was started; the confirm cannot override it, and only the viewer can
1771
+ * raise the limit. This one arrives on the failed snapshot a confirmed
1772
+ * SUBMIT resolves (`{ workflowId: 'failed', status: 'failed', cost, error,
1773
+ * errorCode }`), not on an estimate. On a confirmed step the host also
1774
+ * reports it as the `error` code of `CONFIRMED_STEP_RESULT` — see
1775
+ * {@link BlockRunConfirmedStepHostError}.
1776
+ * - `'workflow_step_denied'` — a step of a `kind: 'workflow'` body names a
1777
+ * platform-internal `$type`. Remove or replace that step.
1778
+ * - `'workflow_ref_invalid'` — a {@link WorkflowStepRef} in a step's `input`
1779
+ * points at the step itself, a later step, or nothing; or a step `name`
1780
+ * breaks the naming rules (see {@link WorkflowBodyWorkflowStep.name}).
1781
+ * `error` names the step by its position.
1782
+ * - `'workflow_too_many_steps'` — more than
1783
+ * {@link BLOCK_WORKFLOW_MAX_STEPS} steps.
1784
+ * - `'workflow_repeat_unbounded'` — a `repeat` step iterates something other
1785
+ * than an array in the body's `arguments`, or more than
1786
+ * {@link BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS} times, nested repeats
1787
+ * multiplied; or the workflow has a `repeat` and the host has no price
1788
+ * quote for it. `error` then reads `workflow step N: a workflow with a
1789
+ * repeat needs a price quote and <cause>`, where `<cause>` is one of the
1790
+ * three in the `workflow_quote_*` messages below. A retry of the same body
1791
+ * can succeed only when the cause is `none could be obtained; try again`;
1792
+ * otherwise the body is what has to change.
1793
+ * - `'workflow_quote_rejected'` — ESTIMATE only. The orchestrator refused
1794
+ * the body, so there is no price to show; resending the same body will not
1795
+ * help. `error` reads `No price quote for this workflow: the orchestrator
1796
+ * rejected the request: <reason>` when the reason is safe to show, else
1797
+ * `No price quote for this workflow: the orchestrator rejected the request;
1798
+ * check the step inputs and references (retrying the same body will not
1799
+ * help)`.
1800
+ * - `'workflow_quote_unavailable'` — ESTIMATE only. No usable answer (an
1801
+ * outage, a timeout, a rate limit, …). The one `workflow_quote_*` code a
1802
+ * retry of the same body can clear. `error` reads `No price quote for this
1803
+ * workflow: none could be obtained; try again`.
1804
+ * - `'workflow_quote_unpriced'` — ESTIMATE only. The orchestrator answered
1805
+ * without a price. `error` reads `No price quote for this workflow: the
1806
+ * orchestrator answered without one`.
1807
+ *
1808
+ * Each `workflow_quote_*` reply is `{ workflowId: 'failed', status: 'failed',
1809
+ * error, errorCode }` with NO `cost`. A host that predates these codes
1810
+ * resolves the estimate instead, with the spend ceiling as `cost.total`.
1518
1811
  *
1519
1812
  * 🔴 AN UNKNOWN VALUE IS POSSIBLE: the host may add codes, and the SDK's
1520
1813
  * inbound validator accepts any string here. Handle an unrecognised code as a
@@ -1722,6 +2015,126 @@ export interface BlockWorkflowSnapshot {
1722
2015
  * that predate civitai/civitai#5434.
1723
2016
  */
1724
2017
  trainingQuote?: BlockTrainingQuote;
2018
+ /**
2019
+ * The viewer-confirmable quote a pass-through `step` ESTIMATE
2020
+ * ({@link WorkflowBodyPassThroughStep}) produced, when the job costs more than
2021
+ * the app may spend without asking. Its presence means: this job cannot run
2022
+ * through `submit()`, and CAN run through `useRunConfirmedStep()` once the
2023
+ * viewer confirms `stepQuote.total` in Civitai's own dialog.
2024
+ *
2025
+ * THE FLOW, mirroring a training run's:
2026
+ * 1. `useBuzzWorkflow().estimate(body)` — the reply carries `stepQuote`.
2027
+ * 2. `useRunConfirmedStep().runConfirmedStep({ ...body, quoteId:
2028
+ * stepQuote.quoteId })` — Civitai shows the viewer the price (read back
2029
+ * from the server, never from the body) and submits on their confirm.
2030
+ * 3. `useBuzzWorkflow().watch(snapshot.workflowId)`.
2031
+ *
2032
+ * WHEN THE HOST SENDS IT — all of these, decided server-side:
2033
+ * - the orchestrator quoted a **FIXED** price for exactly this step:
2034
+ * positive, and not a variable or metered one. The viewer confirms what
2035
+ * they are charged, so a price that may move is never confirmable;
2036
+ * - that price is ABOVE the token's per-call budget and at most
2037
+ * {@link BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB} (5,000 Buzz);
2038
+ * - the block runs in a page app for a signed-in viewer on an ordinary
2039
+ * token. Development, dev-tunnel and review sessions never get one, and
2040
+ * neither does the model slot.
2041
+ * Everywhere else the field is ABSENT and the estimate is exactly what it was.
2042
+ * Absent is therefore not an error — it is also what every host that
2043
+ * predates civitai/civitai#5786 sends.
2044
+ *
2045
+ * 🔴 ONLY A SINGLE ESTIMATE CARRIES IT. A batch estimate
2046
+ * (`ESTIMATE_WORKFLOW_BATCH`, `useBatchEstimate()`) stores nothing, so its
2047
+ * cells never have a `stepQuote`, whatever they cost. Price a grid in a batch,
2048
+ * then estimate the one body you want to run on its own.
2049
+ *
2050
+ * 🔴 TWO BODIES MAKE THAT ESTIMATE FAIL INSTEAD OF RETURNING A PRICE — only
2051
+ * where a quote would otherwise have been granted (so not below the budget,
2052
+ * and not in a batch). The reply is the failure snapshot `{ workflowId:
2053
+ * 'failed', status: 'failed', error }`, which `estimate()` rejects with; the
2054
+ * same text arrives as `CONFIRMED_STEP_RESULT`'s `error` if such a body is
2055
+ * run. Neither has an {@link BlockWorkflowSnapshot.errorCode}; they are
2056
+ * server messages, so match on the text and keep a generic branch:
2057
+ * - `a training step type cannot run as a confirmed step — use
2058
+ * kind:'training'` — `$type` is `training` or `imageResourceTraining`, in
2059
+ * any casing.
2060
+ * Training has its own kind ({@link WorkflowBodyTraining}).
2061
+ * - `a confirmed step needs plain JSON input (objects, arrays, strings,
2062
+ * finite numbers, booleans, null) — <path> is <what>` — `input` holds a
2063
+ * `Date`, `Map`, `Set`, class instance, `BigInt`, `NaN`/`Infinity`, an
2064
+ * `undefined` array element or an empty slot in a sparse array
2065
+ * (`… — body.input.list[1] is an empty array slot`). The quote is bound to the exact body, so the
2066
+ * body must be exactly representable. The part after the dash names the
2067
+ * offending path (`body.input.when is a Date`) and varies: match on the
2068
+ * text BEFORE the dash.
2069
+ *
2070
+ * `total` is the price the viewer confirms and is charged. 🔴 It can differ
2071
+ * from `cost.total` on the same snapshot, which stays what an UNCONFIRMED
2072
+ * submit would reserve (`max(maxBuzz, quote)`); show `stepQuote.total` on a
2073
+ * confirm button. `expiresAt` is an ISO-8601 timestamp 15 minutes out; after
2074
+ * it the run is refused and you estimate again. A quote is single-use.
2075
+ */
2076
+ stepQuote?: BlockStepQuote;
2077
+ /**
2078
+ * What each 3D model in {@link BlockWorkflowSnapshot.imageUrls} IS — one label
2079
+ * per model a pass-through step produced (e.g. `polyGen`).
2080
+ *
2081
+ * Every model url is still listed in `imageUrls`, unchanged and in the same
2082
+ * order as before the field existed; this only says which entry is which.
2083
+ * Match on `url`:
2084
+ *
2085
+ * ```ts
2086
+ * const glb = snapshot.model3dOutputs?.find(
2087
+ * (m) => m.format === 'glb' && m.variant === 'model',
2088
+ * );
2089
+ * if (glb) viewer.load(glb.url);
2090
+ * ```
2091
+ *
2092
+ * OMITTED when the workflow produced no 3D model, and on hosts that predate
2093
+ * civitai/civitai#5786 — in which case `imageUrls` is all there is.
2094
+ */
2095
+ model3dOutputs?: BlockModel3dOutput[];
2096
+ /**
2097
+ * The steps of a `kind: 'workflow'` run ({@link WorkflowBodyWorkflow}), in
2098
+ * the orchestrator's order, each with its own status and results. The steps a
2099
+ * `repeat` step generates follow it, named `<repeat name>.<n>`, so the list
2100
+ * can be longer than the body's.
2101
+ *
2102
+ * The top-level fields keep their meaning: `status` is the workflow's,
2103
+ * `imageUrls` is the union of every step that did NOT set `suppressOutput`,
2104
+ * and `cost` is the whole workflow's.
2105
+ *
2106
+ * 🔴 A `failed` WORKFLOW IS NOT AN EMPTY ONE. Steps that succeeded before the
2107
+ * failing step stay charged and keep their results, so a snapshot with
2108
+ * `status: 'failed'` can carry `cost.total > 0` and usable `imageUrls`. Read
2109
+ * `steps` to see which step failed (`status: 'failed'`, with its `error`)
2110
+ * and which finished.
2111
+ *
2112
+ * Read it from the poll and cancel replies of a `kind: 'workflow'` run; do
2113
+ * not rely on it being on the submit reply. ABSENT for every other kind, on
2114
+ * an estimate (which creates no workflow) and on a host that predates it.
2115
+ */
2116
+ steps?: BlockWorkflowStepSnapshot[];
2117
+ }
2118
+ /** One entry of {@link BlockWorkflowSnapshot.steps}. */
2119
+ export interface BlockWorkflowStepSnapshot {
2120
+ /** The step's `name` from the body, or the name the host gave an unnamed step. */
2121
+ name: string;
2122
+ /** The orchestrator step type that ran. */
2123
+ $type: string;
2124
+ status: 'pending' | 'processing' | 'succeeded' | 'failed' | 'expired' | 'canceled';
2125
+ /**
2126
+ * This step's own image urls. Omitted when it has none, and always omitted
2127
+ * for a step submitted with `suppressOutput: true`.
2128
+ */
2129
+ imageUrls?: string[];
2130
+ /**
2131
+ * The step's raw orchestrator output, minus its blobs — the same treatment
2132
+ * and the same caveats as {@link BlockWorkflowSnapshot.stepOutputs}: its shape
2133
+ * is whatever the orchestrator returns for that `$type`, and it is UNSCANNED.
2134
+ */
2135
+ output?: unknown;
2136
+ /** Why this step failed. Diagnostic text; do not render it verbatim. */
2137
+ error?: string;
1725
2138
  }
1726
2139
  /**
1727
2140
  * The run total a BATCH estimate reports beside its per-cell snapshots
@@ -1755,6 +2168,42 @@ export interface BlockTrainingQuote {
1755
2168
  /** ISO-8601. The quote cannot be run after this. */
1756
2169
  expiresAt: string;
1757
2170
  }
2171
+ /** {@link BlockWorkflowSnapshot.stepQuote}. */
2172
+ export interface BlockStepQuote {
2173
+ /**
2174
+ * `sq_` + 32 lowercase hex characters ({@link BLOCK_STEP_QUOTE_ID_REGEX}).
2175
+ * Opaque — pass it back verbatim as the body's `quoteId`.
2176
+ */
2177
+ quoteId: string;
2178
+ /** The exact price the viewer confirms and is charged, in Buzz. */
2179
+ total: number;
2180
+ /** The orchestrator `$type` that was priced — the body's own `$type`. */
2181
+ stepType: string;
2182
+ /** ISO-8601. The quote cannot be run after this. */
2183
+ expiresAt: string;
2184
+ }
2185
+ /**
2186
+ * One entry of {@link BlockWorkflowSnapshot.model3dOutputs} /
2187
+ * {@link AppWorkflow.model3dOutputs} — the label for one 3D model url.
2188
+ */
2189
+ export interface BlockModel3dOutput {
2190
+ /** The model's url. Also present in the snapshot's `imageUrls` (an `AppWorkflow`'s `images`). */
2191
+ url: string;
2192
+ /**
2193
+ * The model's own file format as the orchestrator reports it — `'glb'`,
2194
+ * `'fbx'`, …. An open set: compare against the formats your viewer loads and
2195
+ * ignore the rest.
2196
+ */
2197
+ format: string;
2198
+ /**
2199
+ * Which output of the step this model is: the output field it came from,
2200
+ * dot-joined when nested — `'model'`, `'riggedFbxModel'`,
2201
+ * `'basicAnimations.walkingModel'`, …. An array index is a segment
2202
+ * (`'models.0'`), and it is `''` when the step's whole output is the model.
2203
+ * The names are the orchestrator's, per `$type`; nothing here lists them.
2204
+ */
2205
+ variant: string;
2206
+ }
1758
2207
  /**
1759
2208
  * Why the host substituted a checkpoint version — see
1760
2209
  * {@link BlockWorkflowSnapshot.modelSubstitutions}.
@@ -1781,7 +2230,7 @@ export type ModelSubstitutionReason = 'wrong-workflow' | 'unrecognized' | 'gated
1781
2230
  * and the SDK's inbound validator accepts any string. Handle an unrecognised
1782
2231
  * code as a generic failure.
1783
2232
  */
1784
- export type KnownBlockWorkflowErrorCode = 'training-quote-rejected' | 'training-quote-unavailable' | 'training-quote-unpriced';
2233
+ export type KnownBlockWorkflowErrorCode = 'training-quote-rejected' | 'training-quote-unavailable' | 'training-quote-unpriced' | 'daily-limit-exceeded' | 'workflow_step_denied' | 'workflow_ref_invalid' | 'workflow_too_many_steps' | 'workflow_repeat_unbounded' | 'workflow_quote_rejected' | 'workflow_quote_unavailable' | 'workflow_quote_unpriced';
1785
2234
  /** One entry of {@link BlockWorkflowSnapshot.modelSubstitutions}. */
1786
2235
  export interface BlockModelSubstitution {
1787
2236
  /** The checkpoint version id the block asked for. */
@@ -2437,6 +2886,13 @@ export interface AppWorkflow {
2437
2886
  * absence as "unknown", not as "no checkpoint".
2438
2887
  */
2439
2888
  trainedEpochs?: BlockTrainedEpoch[];
2889
+ /**
2890
+ * Labels for the 3D models among this workflow's {@link AppWorkflow.images} —
2891
+ * same shape and rule as {@link BlockWorkflowSnapshot.model3dOutputs}: every
2892
+ * labelled `url` is also an entry of `images`, and the field is OMITTED when
2893
+ * there are none (and on hosts that predate it).
2894
+ */
2895
+ model3dOutputs?: BlockModel3dOutput[];
2440
2896
  }
2441
2897
  /**
2442
2898
  * Per-viewer gated projection of ONE image returned by `GET_IMAGES_BY_IDS`
@@ -2754,5 +3210,37 @@ export type BlockPrepareTrainingDatasetHostError = 'review-mode' | 'block is not
2754
3210
  * run is a SECOND, separately charged run.
2755
3211
  */
2756
3212
  export type BlockRunTrainingHostError = 'review-mode' | 'block is not ready' | 'sign in to train' | 'invalid training request' | 'no block token' | 'declined' | 'submission-unconfirmed';
3213
+ /**
3214
+ * The HOST'S OWN refusal codes on a `CONFIRMED_STEP_RESULT`'s `error`. Mirrors
3215
+ * civitai/civitai's `RUN_CONFIRMED_STEP_HOST_ERRORS`
3216
+ * (`src/components/AppBlocks/runConfirmedStepGate.ts`, civitai/civitai#5786).
3217
+ *
3218
+ * 🔴 THE SET A BLOCK MAY RELY ON, NOT THE SET IT MAY RECEIVE: server messages
3219
+ * (an expired or already-used quote, a body that differs from the quoted one, a
3220
+ * price that changed, `confirmed steps from apps are not enabled`) arrive
3221
+ * verbatim in the same field.
3222
+ *
3223
+ * - `review-mode` — a mod-review sandbox with "run for real" off.
3224
+ * - `block is not ready` — the host handshake has not completed.
3225
+ * - `sign in to run` — anonymous viewer; there is no account to charge.
3226
+ * - `invalid confirmed step request` — the body is not a pass-through
3227
+ * `kind: 'step'` body naming an `sq_`-shaped `quoteId`.
3228
+ * - `no block token` — the host holds no block token yet.
3229
+ * - `declined` — the viewer dismissed the confirm. 🔴 GUARANTEED to mean NO JOB
3230
+ * WAS SUBMITTED. The quote is untouched and can still be run before it
3231
+ * expires.
3232
+ * - `daily-limit-exceeded` — the job would exceed what remains today of the
3233
+ * daily Buzz limit the VIEWER set for this app. NO job was submitted and
3234
+ * nothing was charged. The host has already told the viewer, in its own
3235
+ * chrome, and pointed them at where to raise the limit; there is no confirm
3236
+ * to click and nothing the app can do about it. Do not retry.
3237
+ * - `submission-unconfirmed` — the submit was SENT but its outcome is unknown.
3238
+ * 🔴 The job MAY BE RUNNING AND CHARGED. Every confirmation is its own job,
3239
+ * so a retry (which needs a new estimate and a new confirmation) starts a
3240
+ * SECOND, separately charged job if the first one did start. Check the
3241
+ * viewer's generations (`useAppWorkflows()`) before offering one, and never
3242
+ * retry automatically.
3243
+ */
3244
+ export type BlockRunConfirmedStepHostError = 'review-mode' | 'block is not ready' | 'sign in to run' | 'invalid confirmed step request' | 'no block token' | 'declined' | 'daily-limit-exceeded' | 'submission-unconfirmed';
2757
3245
  export {};
2758
3246
  //# sourceMappingURL=types.d.ts.map
@@ -115,6 +115,38 @@ export function isSignedIn(viewer) {
115
115
  return viewer !== null && viewer !== undefined;
116
116
  }
117
117
  // ============================================================
118
+ // Confirmed steps (a pass-through job above the per-call budget)
119
+ // ============================================================
120
+ /**
121
+ * Ceiling on what ONE viewer-confirmed pass-through job may cost, in Buzz. Host
122
+ * constant `BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB`. A fixed price above it gets
123
+ * no {@link BlockWorkflowSnapshot.stepQuote} and cannot be run from an app.
124
+ */
125
+ export const BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB = 5000;
126
+ /**
127
+ * The shape of a confirmed-step quote id ({@link BlockStepQuote.quoteId}): `sq_`
128
+ * followed by 32 lowercase hex characters. Host constant
129
+ * `BLOCK_STEP_QUOTE_ID_REGEX`. Disjoint from the training quote's `tq_` shape,
130
+ * so neither kind's id can name the other's quote.
131
+ */
132
+ export const BLOCK_STEP_QUOTE_ID_REGEX = /^sq_[a-f0-9]{32}$/;
133
+ // ============================================================
134
+ // App Blocks MULTI-STEP kind (`kind: 'workflow'`)
135
+ // ============================================================
136
+ /**
137
+ * The most steps one `kind: 'workflow'` body may carry. The host refuses a
138
+ * longer list with `errorCode: 'workflow_too_many_steps'`; an empty list is
139
+ * refused too.
140
+ */
141
+ export const BLOCK_WORKFLOW_MAX_STEPS = 8;
142
+ /**
143
+ * The most times one top-level `repeat` step of a `kind: 'workflow'` body may
144
+ * run its template, NESTED REPEATS MULTIPLIED (a 4-item repeat inside a 4-item
145
+ * repeat is 16). The host refuses more with
146
+ * `errorCode: 'workflow_repeat_unbounded'`.
147
+ */
148
+ export const BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS = 16;
149
+ // ============================================================
118
150
  // App Blocks TRAINING kind (`kind: 'training'`)
119
151
  // ============================================================
120
152
  //
package/dist/index.d.ts CHANGED
@@ -25,11 +25,11 @@
25
25
  *
26
26
  * - **`./blocks`** — importing it RUNS `./safe-storage` for its side effect
27
27
  * (repairing `localStorage`/`sessionStorage` at an opaque origin). Folding
28
- * 160 symbols and that side effect into the root would make every OAuth app
28
+ * 172 symbols and that side effect into the root would make every OAuth app
29
29
  * that writes `import { exchangeCode } from '@civitai/app-sdk'` pay for the
30
30
  * Civitai-Apps contract it does not use. Blocks and OAuth apps are disjoint
31
31
  * audiences; see this package's README.
32
- * 🔴 That 160 is ASSERTED, not merely quoted — by
32
+ * 🔴 That 172 is ASSERTED, not merely quoted — by
33
33
  * `packages/civitai-app-sdk/test/export-surface.test.ts`, which pins
34
34
  * `surface.get('./blocks').size`. It said 119 here against a real 125 until
35
35
  * 2026-10-04, because the existing checks pin the root as an exact union and
package/dist/index.js CHANGED
@@ -25,11 +25,11 @@
25
25
  *
26
26
  * - **`./blocks`** — importing it RUNS `./safe-storage` for its side effect
27
27
  * (repairing `localStorage`/`sessionStorage` at an opaque origin). Folding
28
- * 160 symbols and that side effect into the root would make every OAuth app
28
+ * 172 symbols and that side effect into the root would make every OAuth app
29
29
  * that writes `import { exchangeCode } from '@civitai/app-sdk'` pay for the
30
30
  * Civitai-Apps contract it does not use. Blocks and OAuth apps are disjoint
31
31
  * audiences; see this package's README.
32
- * 🔴 That 160 is ASSERTED, not merely quoted — by
32
+ * 🔴 That 172 is ASSERTED, not merely quoted — by
33
33
  * `packages/civitai-app-sdk/test/export-surface.test.ts`, which pins
34
34
  * `surface.get('./blocks').size`. It said 119 here against a real 125 until
35
35
  * 2026-10-04, because the existing checks pin the root as an exact union and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/app-sdk",
3
- "version": "0.65.1",
3
+ "version": "0.67.0",
4
4
  "description": "OAuth + PKCE, encrypted-cookie sessions, scopes, and orchestrator helpers for building third-party Civitai apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",