@civitai/app-sdk 0.74.0 → 0.76.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
@@ -32,7 +32,7 @@ pnpm add @civitai/app-sdk
32
32
  | `orchestrator/steps` — `WorkflowStepTemplates`, `WorkflowStepTemplateFor`, `WorkflowStepInputFor`, `AnyWorkflowStepTemplate`, `TypedWorkflowTemplate` | **Type-only** subpath (0 runtime bytes) keying the orchestrator's generated workflow-step shapes from `@civitai/client` by wire `$type`, so apps compose real step bodies against types that track the spec instead of hand-maintained copies. Requires the optional peer `@civitai/client@beta`. Type surface only — it grants no submit permission; see "Typed step shapes" below. |
33
33
  | `blocks/*` — `BlockManifestError`, `BLOCK_SCOPES`, `BLOCK_SCOPE_PATTERN`, `isMessage`, types (`BlockManifestV1`, `BlockContext`, `BlockToken`, `BlockSettings`, `ViewerInfo`, `ThemeInfo`, `BlockWorkflowSnapshot`, `BlockInitPayload`, `ParentToBlockMessage`, `BlockToParentMessage`, …) | Framework-agnostic contract for [Civitai Apps](https://github.com/civitai/civitai-app-starters/blob/main/docs/build-your-first-app-block.md). Ships a byte-identical copy of the server-published canonical JSON Schema (draft 2020-12, https://civitai.com/schemas/app-block/v1.json) at the `./schemas/app-block/v1.json` subpath for offline validation; a CI drift-check keeps it in sync. Runtime-agnostic and **zero runtime dependencies** — no React, no DOM types, nothing in your install graph. Hooks and the iframe transport live in a separate package. |
34
34
  | `manifest/*` — `defineBlock`, `SCHEMA_DIVERGENCES`, `KNOWN_GAPS` (**Node only**) | Build-time manifest validation. `defineBlock(config)` compiles the vendored canonical schema with [Ajv](https://ajv.js.org) and validates a `BlockManifestV1` against it, so authoring mistakes surface in `pnpm dev` instead of at `civitai app validate`/submit. It is **derived from the schema, not a hand-written mirror of it** — that mirror is what produced [#330](https://github.com/civitai/civitai-app-starters/issues/330). Needs `node:fs` and the optional peer `ajv`, which is why it is not on the browser-facing `./blocks` surface. |
35
- | `vite/*` — `blockManifestPlugin` (**Node only**) | A Vite plugin wrapping `defineBlock`, firing from `configResolved` — the one hook Vite calls on both the dev-server and the build path. Every block scaffold registers it, so `pnpm dev`, `pnpm dev:harness` and `pnpm build` all fail on a bad manifest with the offending field path. Optional peers: `ajv` (runtime) and `vite` (types only). |
35
+ | `vite/*` — `blockManifestPlugin` (**Node only**) | A Vite plugin wrapping `defineBlock`, firing from `configResolved` — the one hook Vite calls on both the dev-server and the build path. Every block scaffold registers it, so `pnpm dev`, `pnpm dev:harness` and `pnpm build` all fail on a bad manifest with the offending field path. On the dev server only, it also exposes the manifest's `scopes` + `storage` to `@civitai/sdk` for a dev-tunnel local preview. Optional peers: `ajv` (runtime) and `vite` (types only). |
36
36
 
37
37
  ## Entry points, and what the root barrel is
38
38
 
@@ -407,6 +407,15 @@ import { blockManifestPlugin } from '@civitai/app-sdk/vite';
407
407
  const plugins = [blockManifestPlugin()];
408
408
  ```
409
409
 
410
+ **On the dev server only**, the plugin also puts the manifest's `scopes` and
411
+ `storage` on the page (a `<script>` at the end of `index.html`'s `<head>`; `vite build` never
412
+ emits it). `@civitai/sdk` reads it in a dev-tunnel preview, where the host marks the
413
+ frame `storageMode: 'local-preview'` and `app.storage.collection(name)` is served by
414
+ an in-memory store that enforces these declarations — see "In a dev-tunnel preview"
415
+ in `@civitai/sdk`'s README. Nothing else of the manifest is exposed. Without the
416
+ plugin, a preview's document store refuses every call `local_preview_no_manifest`:
417
+ this plugin is the only way the manifest reaches `@civitai/sdk`.
418
+
410
419
  ### Web storage in a block (`@civitai/app-sdk/safe-storage`)
411
420
 
412
421
  Block iframes are sandboxed **without `allow-same-origin`**, so the document has
@@ -116,7 +116,7 @@ export { isModelSlotContext, isPageSlotContext } from './types.js';
116
116
  * in `types.ts` for which of the two it uses and why.
117
117
  */
118
118
  export { isSignedIn } from './types.js';
119
- export type { BlockContext, KnownSlotId, ModelSlotId, PageSlotContext, PageSlotId, UnknownSlotContext, BlockAnalyticsDeclaration, BlockAnalyticsEventDeclaration, BlockAnalyticsPropertyDeclaration, BlockManifest, BlockManifestGood, BlockManifestV1, BlockSettings, BlockStorageCollectionDeclaration, BlockStorageDeclaration, BlockStorageIndexes, BlockStorageIndexType, BlockStoragePrivateCollectionDeclaration, BlockStorageSharedCollectionDeclaration, BlockToken, ContentRating, ManifestBooleanField, ManifestIframe, ManifestNumberField, ManifestPage, ManifestPreview, ManifestSettingField, ManifestSettings, ManifestStringField, ManifestTarget, ModelSlotContext, SettingScope, SettingWidget, Theme, ViewerInfo, BlockCheckpointInfo, BlockResourceInfo, BlockResourcePickerType, BlockSourceImage, BlockUploadedImageInfo, BlockGenerationSourceImageInfo, BlockImagePickerSource, BlockPickedImageInfo, 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';
119
+ export type { BlockContext, KnownSlotId, ModelSlotId, PageSlotContext, PageSlotId, UnknownSlotContext, BlockAnalyticsDeclaration, BlockAnalyticsEventDeclaration, BlockAnalyticsPropertyDeclaration, BlockManifest, BlockManifestGood, BlockManifestV1, BlockSettings, BlockStorageCollectionDeclaration, BlockStorageDeclaration, BlockStorageIndexes, BlockStorageIndexType, BlockStoragePrivateCollectionDeclaration, BlockStorageSharedCollectionDeclaration, BlockToken, ContentRating, ManifestBooleanField, ManifestIframe, ManifestNumberField, ManifestPage, ManifestPreview, ManifestSettingField, ManifestSettings, ManifestStringField, ManifestTarget, ModelSlotContext, SettingScope, SettingWidget, Theme, ViewerInfo, BlockCheckpointInfo, BlockResourceInfo, BlockResourcePickerType, BlockSourceImage, BlockUploadedImageInfo, BlockGenerationSourceImageInfo, BlockImagePickerSource, BlockPickedImageInfo, BlockMediaKind, BlockMediaUploadInfo, 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';
120
120
  /** Host bound for the App Blocks `kind: 'workflow'` body — see `WorkflowBodyWorkflow`. */
121
121
  export { BLOCK_WORKFLOW_MAX_STEPS, BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS } from './types.js';
122
122
  /**
@@ -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, BlockImagePickerSource, BlockPickedImageInfo, 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';
11
+ import type { BlockCheckpointInfo, BlockResourceInfo, BlockResourcePickerType, BlockUploadedImageInfo, BlockGenerationSourceImageInfo, BlockImagePickerSource, BlockPickedImageInfo, BlockMediaKind, BlockMediaUploadInfo, 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
  import type { BlockAppReaction, BlockEntityReactionErrorCode, BlockEntityReactionResult, BlockEntityReactionsResult, BlockReactionEntityType } from './reactions.js';
13
13
  /**
14
14
  * Filter params for `GET_BUZZ_TRANSACTIONS`. All optional; the host validates
@@ -218,6 +218,21 @@ export interface BlockInitPayload {
218
218
  * them — ship without React.
219
219
  */
220
220
  effectiveBrowsingLevel?: number;
221
+ /**
222
+ * `'local-preview'` when the host mounted this frame as a DEV-TUNNEL preview,
223
+ * which has no server-side storage namespace. The host sets it from the
224
+ * surface it mounted — never from the token's scopes, and never from anything
225
+ * the block sent. Absent on every other surface.
226
+ *
227
+ * `@civitai/sdk`'s `initialize()` reads it once and serves
228
+ * `app.storage.collection(name)` from an in-memory store in the tab
229
+ * (`app.storage.mode === 'local-preview'`). Nothing in this package or in
230
+ * `@civitai/blocks-react` acts on it: their storage hooks keep the real routes.
231
+ *
232
+ * ADDITIVE + OPTIONAL. A host that predates it omits the field; a value other
233
+ * than the exact string must be treated as absent.
234
+ */
235
+ storageMode?: 'local-preview';
221
236
  }
222
237
  /**
223
238
  * The freeform value carried by one SHARED-storage entry. Unlike the per-user
@@ -592,6 +607,13 @@ export type ParentToBlockMessage = {
592
607
  selectedImages?: BlockPickedImageInfo[];
593
608
  error?: string;
594
609
  };
610
+ } | {
611
+ type: 'MEDIA_UPLOAD_RESULT';
612
+ payload: {
613
+ requestId: string;
614
+ selected?: BlockMediaUploadInfo;
615
+ error?: string;
616
+ };
595
617
  } | {
596
618
  type: 'USER_CHECKPOINT_SET';
597
619
  payload: {
@@ -973,6 +995,19 @@ export type BlockToParentMessage = {
973
995
  */
974
996
  publicOnly?: boolean;
975
997
  };
998
+ } | {
999
+ type: 'OPEN_MEDIA_UPLOAD';
1000
+ payload: {
1001
+ requestId: string;
1002
+ /** What kind of media to upload. The host refuses any other value. */
1003
+ kind: BlockMediaKind;
1004
+ /** Required. The only accepted value today. */
1005
+ purpose: 'generationSource';
1006
+ /** In-tab bytes: present = bytes mode (no picker), absent = picker mode. */
1007
+ bytes?: ArrayBuffer;
1008
+ /** Bytes mode only: the type the app believes it is sending; must match the content. */
1009
+ mimeType?: string;
1010
+ };
976
1011
  } | {
977
1012
  type: 'SET_USER_CHECKPOINT';
978
1013
  payload: {
@@ -1194,7 +1229,7 @@ export type BlockToParentMessageType = BlockToParentMessage['type'];
1194
1229
  * it is a measurement, not an invariant, and civitai's own compile-time gate
1195
1230
  * (published SDK union ⊆ `keyof INVENTORY`) is what holds one direction of it.
1196
1231
  */
1197
- 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", "OPEN_IMAGE_PICKER", "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", "SET_ENTITY_REACTION", "GET_ENTITY_REACTIONS"];
1232
+ 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", "OPEN_IMAGE_PICKER", "OPEN_MEDIA_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", "SET_ENTITY_REACTION", "GET_ENTITY_REACTIONS"];
1198
1233
  /**
1199
1234
  * The bucket a message type falls into when the protocol does not declare it.
1200
1235
  *
@@ -79,6 +79,7 @@ export const BLOCK_TO_PARENT_MESSAGE_TYPES = [
79
79
  'OPEN_RESOURCE_PICKER',
80
80
  'OPEN_IMAGE_UPLOAD',
81
81
  'OPEN_IMAGE_PICKER',
82
+ 'OPEN_MEDIA_UPLOAD',
82
83
  'SET_USER_CHECKPOINT',
83
84
  'NAVIGATE',
84
85
  'REQUEST_SIGN_IN',
@@ -372,6 +372,51 @@ export interface BlockPickedImageInfo {
372
372
  */
373
373
  imageId?: number;
374
374
  }
375
+ /**
376
+ * What kind of media `OPEN_MEDIA_UPLOAD` (via `useMediaUpload`) uploads. The
377
+ * host decides the file's real type from its MAGIC BYTES, never from a declared
378
+ * MIME type, and accepts per kind:
379
+ * - `'image'`: PNG, JPEG or WebP;
380
+ * - `'video'`: MP4 or WebM;
381
+ * - `'audio'`: MP3, WAV or WebM.
382
+ */
383
+ export type BlockMediaKind = 'image' | 'video' | 'audio';
384
+ /**
385
+ * One media file the host uploaded for the block as a GENERATION INPUT
386
+ * (`MEDIA_UPLOAD_RESULT.selected`, via `useMediaUpload`).
387
+ *
388
+ * `url` is a PRIVATE generation-input URL: pass it into a workflow step input
389
+ * (an image, video or audio input). It is not a public Civitai image and has no
390
+ * `imageId`. The host refuses data URIs and inline
391
+ * base64 in step inputs, so this is how a block gets media it holds (a picked
392
+ * file, a canvas, a recording) into a step.
393
+ *
394
+ * Per kind:
395
+ * - `'image'`: the host RESIZES the picture so both sides are within 64–2048
396
+ * pixels and RE-ENCODES it as JPEG. `mimeType` is always `'image/jpeg'`,
397
+ * and `sizeBytes`, `width` and `height` describe that JPEG.
398
+ * - `'video'`: uploaded as sent. `width` / `height` are always set (the host
399
+ * refuses a file with no video track); `durationMs` is set when the browser
400
+ * reports a finite duration.
401
+ * - `'audio'`: uploaded as sent. No `width` / `height`; `durationMs` as for
402
+ * video. A `MediaRecorder` WebM can report no duration, so do not rely on it.
403
+ */
404
+ export interface BlockMediaUploadInfo {
405
+ /** The private generation-input URL. Pass it into a step input. */
406
+ url: string;
407
+ /** The kind that was requested. */
408
+ kind: BlockMediaKind;
409
+ /** The type actually uploaded. Always `'image/jpeg'` for `'image'`. */
410
+ mimeType: string;
411
+ /** Bytes actually uploaded (for `'image'`, the re-encoded JPEG's size). */
412
+ sizeBytes: number;
413
+ /** Pixel width. Always set for `'image'` and `'video'`; absent for `'audio'`. */
414
+ width?: number;
415
+ /** Pixel height. Always set for `'image'` and `'video'`; absent for `'audio'`. */
416
+ height?: number;
417
+ /** Duration in milliseconds, for `'video'` / `'audio'` when known. Never set for `'image'`. */
418
+ durationMs?: number;
419
+ }
375
420
  /**
376
421
  * One of the model version's curated preview images, with the standard
377
422
  * gen params extracted from its source meta. Block UIs use these to let
@@ -1295,8 +1340,14 @@ export type WorkflowBodyPassThroughStep = {
1295
1340
  *
1296
1341
  * Consequently the orchestrator's per-`$type` schema is the ONLY authority for
1297
1342
  * what a given `$type` accepts; nothing in this package or on the host mirrors
1298
- * it. Bounded only by size: at most **262144 bytes** (256 KB) serialized, which
1299
- * is a payload-DoS bound and not a shape gate.
1343
+ * it. Its shape is not checked; its size is: at most **262144 bytes** (256 KB)
1344
+ * serialized, which is a payload-DoS bound and not a shape gate.
1345
+ *
1346
+ * 🔴 **No inline media.** The host refuses a `data:image|video|audio` URI or a
1347
+ * long raw base64 string anywhere in here: upload the image, video or audio
1348
+ * with `useMediaUpload()` (an image also works with
1349
+ * `useImageUpload({ purpose: 'generationSource' })` or `useImagePicker()`)
1350
+ * and pass the returned `url`.
1300
1351
  */
1301
1352
  input: Record<string, unknown>;
1302
1353
  /**
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
- * 180 symbols and that side effect into the root would make every OAuth app
28
+ * 198 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 180 is ASSERTED, not merely quoted — by
32
+ * 🔴 That 198 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
- * 180 symbols and that side effect into the root would make every OAuth app
28
+ * 198 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 180 is ASSERTED, not merely quoted — by
32
+ * 🔴 That 198 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
@@ -430,11 +430,14 @@ export interface BuildImageGenBodyOptions {
430
430
  *
431
431
  * @example Nano Banana 2 with a reference image
432
432
  * ```ts
433
+ * // Pass a hosted image URL, not a data URI or raw base64. In an App Block, the
434
+ * // `url` from `useImageUpload({ purpose: 'generationSource' })` or `useImagePicker()`;
435
+ * // the block host refuses inline media in a step input.
433
436
  * const body = buildImageGenBody({
434
437
  * engine: 'google',
435
438
  * model: 'nano-banana-2',
436
439
  * prompt: 'turn this into a cartoon sticker',
437
- * images: ['data:image/png;base64,...'],
440
+ * images: [source.url],
438
441
  * aspectRatio: '1:1',
439
442
  * numImages: 1,
440
443
  * resolution: '1K',
@@ -393,11 +393,14 @@ export function buildTextToImageBody(input, opts = {}) {
393
393
  *
394
394
  * @example Nano Banana 2 with a reference image
395
395
  * ```ts
396
+ * // Pass a hosted image URL, not a data URI or raw base64. In an App Block, the
397
+ * // `url` from `useImageUpload({ purpose: 'generationSource' })` or `useImagePicker()`;
398
+ * // the block host refuses inline media in a step input.
396
399
  * const body = buildImageGenBody({
397
400
  * engine: 'google',
398
401
  * model: 'nano-banana-2',
399
402
  * prompt: 'turn this into a cartoon sticker',
400
- * images: ['data:image/png;base64,...'],
403
+ * images: [source.url],
401
404
  * aspectRatio: '1:1',
402
405
  * numImages: 1,
403
406
  * resolution: '1K',
@@ -1,4 +1,5 @@
1
1
  import type { Plugin } from 'vite';
2
+ import type { BlockManifest } from '../blocks/types.js';
2
3
  export interface BlockManifestPluginOptions {
3
4
  /**
4
5
  * Path to the manifest. Relative paths resolve against `root`, which defaults
@@ -9,6 +10,34 @@ export interface BlockManifestPluginOptions {
9
10
  /** Overrides the Vite project root used to resolve a relative `manifestPath`. */
10
11
  root?: string;
11
12
  }
13
+ /**
14
+ * The global the dev server's HTML sets for `@civitai/sdk` to read. Spelled again
15
+ * in `packages/civitai-sdk/src/doc-store/local-preview.ts` (the two packages share
16
+ * no code); `tests/guards/local-preview-manifest-global.test.mjs` pins the two equal.
17
+ *
18
+ * WHY A GLOBAL, set by a `<script>` the plugin adds to `index.html`, rather than
19
+ * the alternatives:
20
+ * - a VIRTUAL MODULE needs `@civitai/sdk` to import it, which fails to resolve in
21
+ * every bundle not built by this plugin — production included;
22
+ * - a `define` substitution has to reach `@civitai/sdk` inside `node_modules`,
23
+ * where it depends on how the dependency optimizer pre-bundles it, and it would
24
+ * rewrite production builds too;
25
+ * - an `initialize()` option needs every app to import its manifest and pass it,
26
+ * and would be public API.
27
+ * The `<script>` is added on the DEV SERVER only (`vite build` never emits it),
28
+ * needs no change in an app that registers this plugin, and the SDK reads it only
29
+ * when the host marks the frame a local preview. It is the ONLY way the manifest
30
+ * reaches the SDK, so an app that does NOT register the plugin gets the SDK's
31
+ * `local_preview_no_manifest` error. Every scaffold in this repo that ships a
32
+ * manifest registers it; not every scaffold elsewhere does (the CLI's
33
+ * `page-money` template, for one, does not), and those apps must add it.
34
+ */
35
+ export declare const LOCAL_PREVIEW_MANIFEST_GLOBAL = "__CIVITAI_LOCAL_PREVIEW_MANIFEST__";
36
+ /**
37
+ * The classic `<script>` body: only `scopes` and `storage`, as JSON with `<`
38
+ * escaped so no value can close the element.
39
+ */
40
+ export declare function localPreviewManifestScript(manifest: Pick<BlockManifest, 'scopes' | 'storage'>): string;
12
41
  export declare function blockManifestPlugin(options?: BlockManifestPluginOptions): Plugin;
13
42
  export { BlockManifestError } from '../blocks/manifestError.js';
14
43
  //# sourceMappingURL=index.d.ts.map
@@ -18,6 +18,13 @@
18
18
  * pre-check against the same canonical). It exists so a manifest mistake
19
19
  * surfaces in the dev loop rather than at submit time.
20
20
  *
21
+ * DEV SERVER ONLY, it also hands the manifest's `scopes` and `storage` to
22
+ * `@civitai/sdk`, for a dev-tunnel preview. A preview has no server-side storage,
23
+ * so when the host marks the frame `storageMode: 'local-preview'` the SDK serves
24
+ * `app.storage.collection(name)` from an in-memory store that enforces THESE
25
+ * declarations — the ones of the code being previewed, which the server has
26
+ * never seen. See {@link LOCAL_PREVIEW_MANIFEST_GLOBAL} for why it is a global.
27
+ *
21
28
  * PEERS: `ajv` (used at runtime by `@civitai/app-sdk/manifest`) and `vite`
22
29
  * (types only). Both are optional peers of `@civitai/app-sdk`; a scaffold using
23
30
  * this plugin needs both in its devDependencies.
@@ -25,7 +32,60 @@
25
32
  import { readFileSync } from 'node:fs';
26
33
  import { resolve } from 'node:path';
27
34
  import { BlockManifestError, defineBlock } from '../manifest/index.js';
35
+ /**
36
+ * The global the dev server's HTML sets for `@civitai/sdk` to read. Spelled again
37
+ * in `packages/civitai-sdk/src/doc-store/local-preview.ts` (the two packages share
38
+ * no code); `tests/guards/local-preview-manifest-global.test.mjs` pins the two equal.
39
+ *
40
+ * WHY A GLOBAL, set by a `<script>` the plugin adds to `index.html`, rather than
41
+ * the alternatives:
42
+ * - a VIRTUAL MODULE needs `@civitai/sdk` to import it, which fails to resolve in
43
+ * every bundle not built by this plugin — production included;
44
+ * - a `define` substitution has to reach `@civitai/sdk` inside `node_modules`,
45
+ * where it depends on how the dependency optimizer pre-bundles it, and it would
46
+ * rewrite production builds too;
47
+ * - an `initialize()` option needs every app to import its manifest and pass it,
48
+ * and would be public API.
49
+ * The `<script>` is added on the DEV SERVER only (`vite build` never emits it),
50
+ * needs no change in an app that registers this plugin, and the SDK reads it only
51
+ * when the host marks the frame a local preview. It is the ONLY way the manifest
52
+ * reaches the SDK, so an app that does NOT register the plugin gets the SDK's
53
+ * `local_preview_no_manifest` error. Every scaffold in this repo that ships a
54
+ * manifest registers it; not every scaffold elsewhere does (the CLI's
55
+ * `page-money` template, for one, does not), and those apps must add it.
56
+ */
57
+ export const LOCAL_PREVIEW_MANIFEST_GLOBAL = '__CIVITAI_LOCAL_PREVIEW_MANIFEST__';
58
+ /** Reads and validates the manifest, throwing the message Vite should print. */
59
+ function loadManifest(manifestPath) {
60
+ let manifest;
61
+ try {
62
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
63
+ }
64
+ catch (err) {
65
+ throw new Error(`block.manifest.json could not be read or parsed (${manifestPath}): ${err.message}`);
66
+ }
67
+ try {
68
+ defineBlock({ manifest });
69
+ }
70
+ catch (err) {
71
+ // Re-thrown with the field path up front: Vite prints the message, not
72
+ // the error's own properties, so `.field` would otherwise be invisible.
73
+ const field = err instanceof BlockManifestError && err.field ? ` [${err.field}]` : '';
74
+ throw new Error(`block.manifest.json is invalid${field}: ${err.message}`);
75
+ }
76
+ return manifest;
77
+ }
78
+ /**
79
+ * The classic `<script>` body: only `scopes` and `storage`, as JSON with `<`
80
+ * escaped so no value can close the element.
81
+ */
82
+ export function localPreviewManifestScript(manifest) {
83
+ const exposed = JSON.stringify({ scopes: manifest.scopes, storage: manifest.storage }).replace(/</g, '\\u003c');
84
+ return `globalThis[${JSON.stringify(LOCAL_PREVIEW_MANIFEST_GLOBAL)}] = ${exposed};`;
85
+ }
28
86
  export function blockManifestPlugin(options = {}) {
87
+ let manifestPath = '';
88
+ let serving = false;
29
89
  return {
30
90
  name: 'civitai-block-manifest',
31
91
  // `enforce: 'pre'` so the manifest error is the FIRST thing reported rather
@@ -36,23 +96,27 @@ export function blockManifestPlugin(options = {}) {
36
96
  // this module's own location — this file now lives inside the installed
37
97
  // package, not next to the consumer's manifest.
38
98
  const root = options.root ?? config?.root ?? process.cwd();
39
- const manifestPath = resolve(root, options.manifestPath ?? './block.manifest.json');
40
- let manifest;
41
- try {
42
- manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
43
- }
44
- catch (err) {
45
- throw new Error(`block.manifest.json could not be read or parsed (${manifestPath}): ${err.message}`);
46
- }
47
- try {
48
- defineBlock({ manifest });
49
- }
50
- catch (err) {
51
- // Re-thrown with the field path up front: Vite prints the message, not
52
- // the error's own properties, so `.field` would otherwise be invisible.
53
- const field = err instanceof BlockManifestError && err.field ? ` [${err.field}]` : '';
54
- throw new Error(`block.manifest.json is invalid${field}: ${err.message}`);
55
- }
99
+ manifestPath = resolve(root, options.manifestPath ?? './block.manifest.json');
100
+ serving = config?.command === 'serve';
101
+ loadManifest(manifestPath);
102
+ },
103
+ transformIndexHtml() {
104
+ // Dev server only: a build never carries the manifest into the page.
105
+ if (!serving)
106
+ return undefined;
107
+ // Re-read per page load, so an edited manifest takes effect on reload.
108
+ const manifest = loadManifest(manifestPath);
109
+ return [
110
+ {
111
+ tag: 'script',
112
+ // End of <head>, NOT `head-prepend`: prepended, a large `storage` block
113
+ // could push `<meta charset>` past the 1024-byte prescan. Nothing needs
114
+ // it earlier: the SDK reads the global inside `initialize()`, after
115
+ // BLOCK_INIT, and a classic script still runs before any module script.
116
+ injectTo: 'head',
117
+ children: localPreviewManifestScript(manifest),
118
+ },
119
+ ];
56
120
  },
57
121
  };
58
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/app-sdk",
3
- "version": "0.74.0",
3
+ "version": "0.76.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",