@civitai/app-sdk 0.70.1 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -191,7 +191,9 @@ own key:
191
191
  `$type` cap, a 256 KB `input` cap, and the spend reservation. **Unlike the inline arm, `maxBuzz`
192
192
  is not always the timeout here**: on both `estimate` and `submit` the host asks the orchestrator
193
193
  for a `whatif` quote of the step. **Quoted** → it reserves `max(maxBuzz, quote)`, gated against
194
- the token's per-call budget (`token.buzzBudget`, capped at 1000 on a production token — so the
194
+ the token's per-call budget (`token.buzzBudget`: a platform default when the manifest declares
195
+ none, clamped to a platform cap — current values in the
196
+ [limits table](https://developer.civitai.com/apps/guide/text-to-image.md#limits-table) — so the
195
197
  reservation can exceed the 1–250 `maxBuzz` range), and stamps **no** step timeout.
196
198
  **Unquoted** → `maxBuzz` is both the reservation and the step timeout in seconds; the timeout
197
199
  bounds wall-clock time, and spend only for a step billed by compute time — a step priced per
@@ -109,7 +109,7 @@ 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, 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';
112
+ 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';
113
113
  /** Host bound for the App Blocks `kind: 'workflow'` body — see `WorkflowBodyWorkflow`. */
114
114
  export { BLOCK_WORKFLOW_MAX_STEPS, BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS } from './types.js';
115
115
  /**
@@ -938,6 +938,25 @@ export type BlockToParentMessage = {
938
938
  multiple?: {
939
939
  max: number;
940
940
  };
941
+ /**
942
+ * Offer only images in a published, non-`Private` post: `true`
943
+ * narrows the `'posted'` library to those. It does not filter by
944
+ * rating or by any other post setting. The `'generated'` library is
945
+ * unaffected. OMIT it (or `false`) for today's behaviour, which also
946
+ * offers images in a `Private` post. Send a boolean or nothing: the
947
+ * host refuses any other value with `{ requestId, error }`.
948
+ *
949
+ * Set it when the pick becomes PUBLIC-FACING, e.g. an App Store
950
+ * sub-listing card image (`imageId` on
951
+ * `POST /api/v1/blocks/sub-listings/upsert`, `app.subListings.upsert`
952
+ * in `@civitai/sdk`), which the store refuses for a non-public image.
953
+ * Leave it off when the pick is a private input (a generation source).
954
+ *
955
+ * 🔴 A HOST THAT PREDATES THIS FIELD IGNORES IT and still offers
956
+ * `Private`-post images, so an app setting it must still handle the
957
+ * store's `image_not_public` refusal.
958
+ */
959
+ publicOnly?: boolean;
941
960
  };
942
961
  } | {
943
962
  type: 'SET_USER_CHECKPOINT';
@@ -20,6 +20,8 @@ export declare const BLOCK_SCOPES: {
20
20
  readonly APPS_STORAGE_WRITE: "apps:storage:write";
21
21
  readonly APPS_STORAGE_SHARED_READ: "apps:storage:shared:read";
22
22
  readonly APPS_STORAGE_SHARED_WRITE: "apps:storage:shared:write";
23
+ readonly APPS_STORAGE: "apps:storage";
24
+ readonly APPS_STORAGE_PUBLISH: "apps:storage:publish";
23
25
  readonly COLLECTIONS_READ_SELF: "collections:read:self";
24
26
  readonly COLLECTIONS_WRITE_SELF: "collections:write:self";
25
27
  readonly COLLECTIONS_READ_PRIVATE: "collections:read:private";
@@ -31,8 +33,9 @@ export declare const BLOCK_SCOPES: {
31
33
  export type BlockScopeKey = keyof typeof BLOCK_SCOPES;
32
34
  export type BlockScope = (typeof BLOCK_SCOPES)[BlockScopeKey];
33
35
  /**
34
- * Format helper for the block-scope shape — 3 OR 4 colon-separated lowercase
35
- * segments (`domain:verb:target` or `domain:area:verb:target`).
36
+ * Format helper for the block-scope shape — 2, 3 OR 4 colon-separated
37
+ * lowercase segments (`domain:area`, `domain:verb:target` or
38
+ * `domain:area:verb:target`).
36
39
  *
37
40
  * NOTE: this regex is **not** the authoritative validity contract. The
38
41
  * canonical manifest schema (https://civitai.com/schemas/app-block/v1.json)
@@ -48,6 +51,8 @@ export type BlockScope = (typeof BLOCK_SCOPES)[BlockScopeKey];
48
51
  * cross-user datastore); `apps:store:items:write` is the same shape. The SDK
49
52
  * now tracks the server's 4-segment scopes directly, so this is no longer a coordinated-major-bump situation — the
50
53
  * pattern just widened to accept the shapes the canonical enum already lists.
54
+ * It widened again, at the other end, for the 2-segment `apps:storage`
55
+ * (the document store's base scope).
51
56
  */
52
57
  export declare const BLOCK_SCOPE_PATTERN: RegExp;
53
58
  /**
@@ -27,6 +27,18 @@ export const BLOCK_SCOPES = {
27
27
  // 4-segment (as is `apps:store:items:write`) — see BLOCK_SCOPE_PATTERN below.
28
28
  APPS_STORAGE_SHARED_READ: 'apps:storage:shared:read',
29
29
  APPS_STORAGE_SHARED_WRITE: 'apps:storage:shared:write',
30
+ // apps:storage / apps:storage:publish — the DOCUMENT STORE, declared per
31
+ // collection in the manifest's `storage` block, that succeeds the four
32
+ // scopes above (which stay live until apps migrate). Same no-OAuth-bit
33
+ // posture. `apps:storage` covers reads, the viewer's private collections,
34
+ // marks and counters, and is CONSENT-EXEMPT; any manifest declaring a
35
+ // collection must request it. `apps:storage:publish` writes documents in a
36
+ // SHARED collection under the viewer's identity — SENSITIVE (needs a
37
+ // `scopeJustifications` entry) and CONSENT-GATED. `apps:storage` is the one
38
+ // 2-segment scope — see BLOCK_SCOPE_PATTERN below. Server source:
39
+ // civitai/civitai `src/shared/constants/block-scope.constants.ts`.
40
+ APPS_STORAGE: 'apps:storage',
41
+ APPS_STORAGE_PUBLISH: 'apps:storage:publish',
30
42
  // collections:* — the App Blocks Collections surface. These have no OAuth bit
31
43
  // (they never touch the user's civitai resources via the OAuth surface); the
32
44
  // server gates them per-op server-side (collection visibility/ownership +
@@ -80,8 +92,9 @@ export const BLOCK_SCOPES = {
80
92
  APPS_STORE_ITEMS_WRITE: 'apps:store:items:write',
81
93
  };
82
94
  /**
83
- * Format helper for the block-scope shape — 3 OR 4 colon-separated lowercase
84
- * segments (`domain:verb:target` or `domain:area:verb:target`).
95
+ * Format helper for the block-scope shape — 2, 3 OR 4 colon-separated
96
+ * lowercase segments (`domain:area`, `domain:verb:target` or
97
+ * `domain:area:verb:target`).
85
98
  *
86
99
  * NOTE: this regex is **not** the authoritative validity contract. The
87
100
  * canonical manifest schema (https://civitai.com/schemas/app-block/v1.json)
@@ -97,8 +110,10 @@ export const BLOCK_SCOPES = {
97
110
  * cross-user datastore); `apps:store:items:write` is the same shape. The SDK
98
111
  * now tracks the server's 4-segment scopes directly, so this is no longer a coordinated-major-bump situation — the
99
112
  * pattern just widened to accept the shapes the canonical enum already lists.
113
+ * It widened again, at the other end, for the 2-segment `apps:storage`
114
+ * (the document store's base scope).
100
115
  */
101
- export const BLOCK_SCOPE_PATTERN = /^[a-z]+:[a-z]+:[a-z]+(?::[a-z]+)?$/;
116
+ export const BLOCK_SCOPE_PATTERN = /^[a-z]+:[a-z]+(?::[a-z]+){0,2}$/;
102
117
  /**
103
118
  * Marketplace category taxonomy for the optional manifest `category` field —
104
119
  * the app's `/apps` store listing bucket. Mirrors `MARKETPLACE_CATEGORIES` in
@@ -304,6 +304,11 @@ export type BlockGenerationSourceImageInfo = BlockSourceImage;
304
304
  *
305
305
  * The request's `sources` narrows the picker to one of them; omitting it offers
306
306
  * both. Images only — neither library offers video through this picker.
307
+ *
308
+ * The request's `publicOnly: true` further narrows `'posted'` to images whose
309
+ * post is published and not `Private` (for a public-facing pick such as an App
310
+ * Store sub-listing card image); it does not change `'generated'`. A host that
311
+ * predates `publicOnly` ignores it.
307
312
  */
308
313
  export type BlockImagePickerSource = 'posted' | 'generated';
309
314
  /**
@@ -314,7 +319,8 @@ export type BlockImagePickerSource = 'posted' | 'generated';
314
319
  *
315
320
  * WHAT IS PICKABLE is decided by the host, never by the block:
316
321
  * - `'posted'`: the viewer's own images that are in a published post, fully
317
- * scanned, not flagged, and rated.
322
+ * scanned, not flagged, and rated. A `Private` post counts as published
323
+ * here unless the request set `publicOnly: true`, which leaves those out.
318
324
  * - `'generated'`: the viewer's own succeeded generator outputs that carry a
319
325
  * rating the host recognises.
320
326
  * - Either way the image's rating has to be one the VIEWER'S OWN browsing
@@ -1305,9 +1311,12 @@ export type WorkflowBodyPassThroughStep = {
1305
1311
  * - **Quoted** — the host reserves `max(maxBuzz, quote)` and stamps NO step
1306
1312
  * timeout. That reservation can exceed 250: it is gated against the token's
1307
1313
  * per-call budget (`token.buzzBudget` — the app's per-generation budget,
1308
- * capped at 1000 when the host mints a production token), and a submit
1309
- * whose reservation exceeds that budget comes back `failed` with an
1310
- * `insufficient buzz budget` error before any Buzz is reserved.
1314
+ * i.e. `page.buzzBudgetPerGen` for a page app: a platform default when
1315
+ * nothing declares one, and clamped to a platform cap on every mint), and
1316
+ * a submit whose reservation exceeds that budget comes back `failed` with
1317
+ * an `insufficient buzz budget` error before any Buzz is reserved. The
1318
+ * current default and cap are in the pinned limits table,
1319
+ * https://developer.civitai.com/apps/guide/text-to-image.md#limits-table.
1311
1320
  * - **Unquoted** (the orchestrator returned no price, or the quote could not
1312
1321
  * be had) — the host reserves `maxBuzz` and stamps it as the step timeout,
1313
1322
  * in seconds. `maxBuzz: 10` then buys a job that is KILLED after 10
@@ -2534,6 +2543,141 @@ export interface BlockAnalyticsDeclaration {
2534
2543
  */
2535
2544
  events?: Record<string, BlockAnalyticsEventDeclaration>;
2536
2545
  }
2546
+ /**
2547
+ * The type of one storage index — see {@link BlockStorageIndexes}. A `string`
2548
+ * index holds values of at most 128 bytes. Mirrors the canonical schema's
2549
+ * `storage.collections.*.indexes.*` enum.
2550
+ */
2551
+ export type BlockStorageIndexType = 'string' | 'number' | 'boolean' | 'time';
2552
+ /**
2553
+ * A collection's `indexes`: fields of a document's `data` the app can filter and
2554
+ * sort by, as a map of field PATH → {@link BlockStorageIndexType}. At most 4.
2555
+ * A path is 1 to 3 segments joined by `.` (`status`, `datasetId`,
2556
+ * `meta.round`), each a letter followed by up to 31 letters, digits or `_`; a
2557
+ * field whose own name contains `.` cannot be indexed. Changing an index's
2558
+ * type is not supported: remove it and add one under a new name.
2559
+ *
2560
+ * `key`, `createdAt` and `updatedAt` are built-in sort keys and cannot be index
2561
+ * names — the canonical declares each as `false`, and they are typed `never`
2562
+ * here so declaring one is a compile error. (That is also why the index
2563
+ * signature admits `undefined`: TypeScript requires an optional property to be
2564
+ * assignable to the signature.) Mirrors the canonical schema's
2565
+ * `storage.collections.*.indexes`.
2566
+ */
2567
+ export interface BlockStorageIndexes {
2568
+ [path: string]: BlockStorageIndexType | undefined;
2569
+ key?: never;
2570
+ createdAt?: never;
2571
+ updatedAt?: never;
2572
+ }
2573
+ /**
2574
+ * A PRIVATE collection — a separate key space per viewer, read and written
2575
+ * only by that viewer. Carries none of the shared-only fields (`write`,
2576
+ * `counters`, `marks`, `showAuthor`, `dailyWritesPerAuthor`); the canonical
2577
+ * forbids them here. See {@link BlockStorageSharedCollectionDeclaration} for
2578
+ * the fields both kinds share.
2579
+ */
2580
+ export interface BlockStoragePrivateCollectionDeclaration {
2581
+ /** See {@link BlockStorageSharedCollectionDeclaration.kind}. */
2582
+ kind?: 'docs' | 'log';
2583
+ /** Cannot be changed later: declare a new collection instead. */
2584
+ visibility: 'private';
2585
+ /** See {@link BlockStorageIndexes}. */
2586
+ indexes?: BlockStorageIndexes;
2587
+ /** RESERVED — see {@link BlockStorageSharedCollectionDeclaration.ttlSeconds}. */
2588
+ ttlSeconds?: number;
2589
+ /** RESERVED — see {@link BlockStorageSharedCollectionDeclaration.maxEntries}. */
2590
+ maxEntries?: number;
2591
+ }
2592
+ /**
2593
+ * A SHARED collection — one key space for the whole app, read by every viewer
2594
+ * of the app, signed-out viewers included. `write` is REQUIRED on it.
2595
+ */
2596
+ export interface BlockStorageSharedCollectionDeclaration {
2597
+ /**
2598
+ * `'docs'` (the default): documents with app-chosen or server-assigned keys
2599
+ * that can be updated in place. `'log'`: append-only entries, trimmed to
2600
+ * `maxEntries` per key space. RESERVED: civitai.com refuses `'log'` until log
2601
+ * collections are enabled, so declare `'docs'` or omit it.
2602
+ */
2603
+ kind?: 'docs' | 'log';
2604
+ /** Cannot be changed later: declare a new collection instead. */
2605
+ visibility: 'shared';
2606
+ /**
2607
+ * Who may create, update and delete documents.
2608
+ *
2609
+ * - `'own'` — eligible signed-in viewers create documents; only a document's
2610
+ * author updates or deletes it.
2611
+ * - `'none'` — nobody writes documents; viewers only increment the declared
2612
+ * `counters` and set the declared `marks`. Must declare at least one
2613
+ * counter or mark.
2614
+ * - `'anyone'` — RESERVED (refused at submit until enabled): eligible
2615
+ * signed-in viewers update or delete any document.
2616
+ * - `'owner'` — RESERVED (refused at submit until enabled): only the app
2617
+ * owner writes documents; viewers can still mark and count.
2618
+ *
2619
+ * `'own'` and `'anyone'` require the `apps:storage:publish` scope.
2620
+ */
2621
+ write: 'own' | 'anyone' | 'owner' | 'none';
2622
+ /**
2623
+ * Names of the atomic counters each document carries (for example `plays`).
2624
+ * At most 8 distinct lowercase snake_case names (`^[a-z][a-z0-9_]{0,31}$`),
2625
+ * never `report`, and never a name also used by a mark.
2626
+ */
2627
+ counters?: string[];
2628
+ /**
2629
+ * Names of the per-viewer marks a viewer can set on a document (for example
2630
+ * `vote`), at most one of each per viewer per document. A reader sees each
2631
+ * mark's count and whether THEY set it, never who else did. At most 8
2632
+ * distinct lowercase snake_case names. `report` is built in on every shared
2633
+ * collection and must NOT be declared.
2634
+ */
2635
+ marks?: string[];
2636
+ /** See {@link BlockStorageIndexes}. */
2637
+ indexes?: BlockStorageIndexes;
2638
+ /**
2639
+ * When true, readers see each viewer-authored document's author USERNAME.
2640
+ * Default false. The numeric user id is never exposed either way.
2641
+ */
2642
+ showAuthor?: boolean;
2643
+ /**
2644
+ * RESERVED — refused at submit until expiring documents are enabled. The
2645
+ * default document lifetime in seconds, 60 to 31536000 (provisional bounds).
2646
+ */
2647
+ ttlSeconds?: number;
2648
+ /**
2649
+ * RESERVED — log collections are refused at submit. For `kind: 'log'` only:
2650
+ * the most entries kept per key space, 1 to 10000 (omitted means 10000).
2651
+ */
2652
+ maxEntries?: number;
2653
+ /**
2654
+ * RESERVED — refused at submit until per-collection write budgets are
2655
+ * enabled. The per-viewer daily create/update budget, 1 to 1000, for a
2656
+ * collection viewers write (`write` `'own'` or `'anyone'` only).
2657
+ */
2658
+ dailyWritesPerAuthor?: number;
2659
+ }
2660
+ /**
2661
+ * One collection declaration — see {@link BlockManifestV1.storage}.
2662
+ * Discriminated on `visibility`, mirroring the canonical's `if visibility is
2663
+ * "shared" then write is required, else no shared-only field` rule. Mirrors the
2664
+ * canonical schema's `storage.collections.*`.
2665
+ */
2666
+ export type BlockStorageCollectionDeclaration = BlockStoragePrivateCollectionDeclaration | BlockStorageSharedCollectionDeclaration;
2667
+ /**
2668
+ * The manifest's `storage` object — see {@link BlockManifestV1.storage}.
2669
+ * Mirrors the canonical schema's `storage`; no other key is allowed.
2670
+ */
2671
+ export interface BlockStorageDeclaration {
2672
+ /**
2673
+ * Collection name → declaration. At least 1 and at most 16 collections. A
2674
+ * collection name is lowercase snake_case: a letter, then up to 31 of `a-z`,
2675
+ * `0-9` or `_` (`^[a-z][a-z0-9_]{0,31}$`). Renaming a collection, or changing
2676
+ * its visibility, is a NEW collection: the old one's documents are not
2677
+ * carried over.
2678
+ */
2679
+ collections: Record<string, BlockStorageCollectionDeclaration>;
2680
+ }
2537
2681
  /**
2538
2682
  * One entry of a manifest's `goods[]`. Mirrors the canonical schema exactly;
2539
2683
  * `id`, `title` and `priceBuzz` are required there and so are they here.
@@ -2673,6 +2817,26 @@ export interface BlockManifestV1 {
2673
2817
  * canonical schema's `analytics`.
2674
2818
  */
2675
2819
  analytics?: BlockAnalyticsDeclaration;
2820
+ /**
2821
+ * Optional APP STORAGE declaration — every collection of JSON documents the
2822
+ * app keeps, and who may read and write each one. Manifest-governed and
2823
+ * REVIEW-GATED: a collection not declared here does not exist at runtime.
2824
+ *
2825
+ * Declaring any collection requires the `apps:storage` scope; a shared
2826
+ * collection viewers write (`write` `'own'` or `'anyone'`) also requires the
2827
+ * sensitive, consent-gated `apps:storage:publish` scope with a
2828
+ * `scopeJustifications` entry; and requesting either scope with nothing here
2829
+ * for it to reach is rejected. Those couplings, and the RESERVED features
2830
+ * (`kind: 'log'`, `ttlSeconds`, `dailyWritesPerAuthor`, `write` `'anyone'` /
2831
+ * `'owner'`), are enforced by the platform's manifest validator at submit,
2832
+ * not by `defineBlock` — see `KNOWN_GAPS['storage-server-only-rules']`.
2833
+ *
2834
+ * This TYPES the declaration only. Storage is not yet enabled on civitai.com
2835
+ * (its routes answer `503 storage_disabled`); the published schema and the Go
2836
+ * CLI already accept the field. Kept in lockstep with the canonical schema's
2837
+ * `storage`.
2838
+ */
2839
+ storage?: BlockStorageDeclaration;
2676
2840
  /** Optional; the canonical declares no required sub-field. */
2677
2841
  iframe?: ManifestIframe;
2678
2842
  /** Full-page surface descriptor (W10). */
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
- * 174 symbols and that side effect into the root would make every OAuth app
28
+ * 180 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 174 is ASSERTED, not merely quoted — by
32
+ * 🔴 That 180 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
- * 174 symbols and that side effect into the root would make every OAuth app
28
+ * 180 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 174 is ASSERTED, not merely quoted — by
32
+ * 🔴 That 180 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
@@ -96,6 +96,7 @@ export declare const KNOWN_GAPS: {
96
96
  readonly 'review-granted-scope-subset': "Which of the requested `scopes` review actually grants has no schema expression at all.";
97
97
  readonly 'slotId-registry': string;
98
98
  readonly 'settings-meta-schema-not-vendored': string;
99
+ readonly 'storage-server-only-rules': string;
99
100
  readonly 'format-uri-not-enforced': string;
100
101
  };
101
102
  /** The canonical schema object, parsed once. Exported for tests and tooling. */
@@ -143,6 +143,13 @@ export const KNOWN_GAPS = {
143
143
  'settings-meta-schema-not-vendored': "civitai/civitai's `manifest-settings.meta.schema.ts` is not published at a URL and not " +
144
144
  'vendored here, so `settings` is the one surface still checked by a hand-written mirror. ' +
145
145
  'See SCHEMA_DIVERGENCES.settings.',
146
+ 'storage-server-only-rules': 'The canonical `storage` description states rules it does not encode: declaring any ' +
147
+ 'collection requires the `apps:storage` scope, a shared collection viewers write (`write` ' +
148
+ '"own" or "anyone") also requires `apps:storage:publish`, requesting either scope with no ' +
149
+ 'collection for it to reach is rejected, a counter may not share a name with a mark, and the ' +
150
+ 'RESERVED features (`kind` "log", `ttlSeconds`, `dailyWritesPerAuthor`, `write` "anyone" / ' +
151
+ '"owner") validate offline and are refused at submit. Ajv enforces the structural rules ' +
152
+ 'exactly; these are server-side.',
146
153
  'format-uri-not-enforced': '`assetBundleUrl` carries `format: "uri"` in addition to `pattern: "^https://"`. Ajv ignores ' +
147
154
  'unknown formats unless `ajv-formats` is installed, which would be a second optional peer for ' +
148
155
  'one field whose pattern is already enforced. The pattern IS enforced; the `uri` format is ' +
@@ -15,5 +15,5 @@
15
15
  export { defineBlock, SCHEMA_DIVERGENCES, KNOWN_GAPS, loadCanonicalSchema } from './defineBlock.js';
16
16
  export type { DefineBlockConfig } from './defineBlock.js';
17
17
  export { BlockManifestError } from '../blocks/manifestError.js';
18
- export type { BlockAnalyticsDeclaration, BlockAnalyticsEventDeclaration, BlockAnalyticsPropertyDeclaration, BlockManifest, BlockManifestGood, BlockManifestV1, } from '../blocks/types.js';
18
+ export type { BlockAnalyticsDeclaration, BlockAnalyticsEventDeclaration, BlockAnalyticsPropertyDeclaration, BlockManifest, BlockManifestGood, BlockManifestV1, BlockStorageCollectionDeclaration, BlockStorageDeclaration, BlockStorageIndexes, BlockStorageIndexType, BlockStoragePrivateCollectionDeclaration, BlockStorageSharedCollectionDeclaration, } from '../blocks/types.js';
19
19
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/app-sdk",
3
- "version": "0.70.1",
3
+ "version": "0.72.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",
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "auth": {
74
74
  "type": "string",
75
- "description": "Which credential the host hands the block: \"block-token\" (default when omitted) is the block-scoped JWT; \"oauth\" asks for an opaque OAuth access token for the block's own OauthClient, which ordinary /api/v1 routes accept as a viewer bearer. Minting it is gated on a server flag that is not generally enabled, and also requires a signed-in viewer and a declared \"user:read:self\" scope; when any of those is unmet the host hands back a block JWT instead of failing, so branch on the token kind the host reports rather than on this field. An OAuth token is refused by the postMessage bridge procedures, which verify a block JWT only, and a manifest declaring \"oauth\" alongside any \"apps:storage:*\" scope is rejected by manifest validation.",
75
+ "description": "Which credential the host hands the block: \"block-token\" (default when omitted) is the block-scoped JWT; \"oauth\" asks for an opaque OAuth access token for the block's own OauthClient, which ordinary /api/v1 routes accept as a viewer bearer. Minting it is gated on a server flag that is not generally enabled, and also requires a signed-in viewer and a declared \"user:read:self\" scope; when any of those is unmet the host hands back a block JWT instead of failing, so branch on the token kind the host reports rather than on this field. An OAuth token is refused by the postMessage bridge procedures, which verify a block JWT only, and a manifest declaring \"oauth\" alongside \"apps:storage\" or any \"apps:storage:*\" scope is rejected by manifest validation.",
76
76
  "enum": ["block-token", "oauth"]
77
77
  },
78
78
  "trustTier": {
@@ -96,6 +96,8 @@
96
96
  "apps:storage:write",
97
97
  "apps:storage:shared:read",
98
98
  "apps:storage:shared:write",
99
+ "apps:storage",
100
+ "apps:storage:publish",
99
101
  "collections:read:self",
100
102
  "collections:write:self",
101
103
  "collections:read:private",
@@ -108,7 +110,7 @@
108
110
  },
109
111
  "scopeJustifications": {
110
112
  "type": "object",
111
- "description": "Per-scope justification: a map of scope-id → free-text rationale explaining WHY the app needs that permission, shown to the moderator during review. REQUIRED for SENSITIVE scopes — any declared scope that can spend or read the viewer's Buzz, read the viewer's private data, or write data other users see (e.g. `ai:write:budgeted`, `social:tip:self`, `goods:purchase:self`, `buzz:read:self`, `collections:read:private`, `apps:storage:shared:write`, `posts:write:self`, `apps:store:items:write`) MUST carry a non-empty justification here, or the manifest is rejected at submit time. OPTIONAL for non-sensitive scopes — omit those and the manifest stays valid. Every key MUST be a scope also present in `scopes` (justifications for scopes you don't request are rejected). Each value is a non-empty string of at most 500 characters. The requirement is enforced imperatively by the manifest validator (not expressed as JSON-Schema conditionals here). NOTE: the justification captures the developer's STATED rationale only; the platform does not verify the truth of the claims.",
113
+ "description": "Per-scope justification: a map of scope-id → free-text rationale explaining WHY the app needs that permission, shown to the moderator during review. REQUIRED for SENSITIVE scopes — any declared scope that can spend or read the viewer's Buzz, read the viewer's private data, or write data other users see (e.g. `ai:write:budgeted`, `social:tip:self`, `goods:purchase:self`, `buzz:read:self`, `collections:read:private`, `apps:storage:shared:write`, `apps:storage:publish`, `posts:write:self`, `apps:store:items:write`) MUST carry a non-empty justification here, or the manifest is rejected at submit time. OPTIONAL for non-sensitive scopes — omit those and the manifest stays valid. Every key MUST be a scope also present in `scopes` (justifications for scopes you don't request are rejected). Each value is a non-empty string of at most 500 characters. The requirement is enforced imperatively by the manifest validator (not expressed as JSON-Schema conditionals here). NOTE: the justification captures the developer's STATED rationale only; the platform does not verify the truth of the claims.",
112
114
  "additionalProperties": {
113
115
  "type": "string",
114
116
  "minLength": 1,
@@ -219,7 +221,7 @@
219
221
  },
220
222
  "buzzBudgetPerGen": {
221
223
  "type": "integer",
222
- "description": "Optional per-generation Buzz SAFETY CEILING for `ai:write:budgeted` tokens — the most a SINGLE generation this app requests is allowed to cost. It is a ceiling against a malicious or compromised app draining the viewer's Buzz, NOT a cost forecast: size it WELL ABOVE your worst-case generation (a good rule of thumb is several times your expected cost — e.g. 1000 when a run costs ~100), never at your estimate. Setting it to an estimate is the common mistake. The server re-prices every submit and compares that real price against this budget BEFORE anything runs, so a generation that comes in over budget is REJECTED outright with `insufficient buzz budget` — no workflow is created and no Buzz is spent, but the user gets nothing, and it stays broken for EVERY user until you ship a new manifest version and it is re-approved. So any upward drift in real cost (more steps, a bigger resolution, a pricier model or recipe, a colder cache) turns a budget sized to today's estimate into a hard outage. Positive integer. Omit it and the token is minted with a 10 Buzz default, which is below almost any real generation. Server-clamped to the platform per-gen cap (1000 today), so a generous value is safe. This bounds ONE generation only — cumulative spend is separately capped per viewer per day and per app, so a high per-gen ceiling does not widen total exposure.",
224
+ "description": "Optional per-generation Buzz SAFETY CEILING for `ai:write:budgeted` tokens — the most a SINGLE generation this app requests is allowed to cost. It is a ceiling against a malicious or compromised app draining the viewer's Buzz, NOT a cost forecast: size it WELL ABOVE your worst-case generation (a good rule of thumb is several times your expected cost — e.g. 1000 when a run costs ~100), never at your estimate. Setting it to an estimate is the common mistake. The server re-prices every submit and compares that real price against this budget BEFORE anything runs, so a generation that comes in over budget is REJECTED outright with `insufficient buzz budget` — no workflow is created and no Buzz is spent, but the user gets nothing, and it stays broken for EVERY user until you ship a new manifest version and it is re-approved. So any upward drift in real cost (more steps, a bigger resolution, a pricier model or recipe, a colder cache) turns a budget sized to today's estimate into a hard outage. Positive integer. Omit it and the token is minted with the platform default budget. Server-clamped to the platform per-call cap, so a generous value is safe. The current default and cap are listed in the App Blocks developer docs (https://developer.civitai.com/apps). This bounds ONE generation only — cumulative spend is separately capped per viewer per day and per app, so a high per-gen ceiling does not widen total exposure.",
223
225
  "exclusiveMinimum": 0
224
226
  }
225
227
  }
@@ -373,6 +375,154 @@
373
375
  }
374
376
  }
375
377
  },
378
+ "storage": {
379
+ "type": "object",
380
+ "description": "Optional APP STORAGE declaration — every collection of JSON documents your app keeps, and who may read and write each one. Manifest-governed and REVIEW-GATED: a collection that is not declared here does not exist at runtime, and the moderator sees each collection's visibility, write policy, counters, marks and indexes. Declaring any collection also requires the `apps:storage` scope; declaring a shared collection that viewers write (`write` of \"own\" or \"anyone\") also requires the sensitive, consent-gated `apps:storage:publish` scope with a `scopeJustifications` entry; and requesting either scope with nothing here for it to reach is rejected. This schema describes the WHOLE declaration, including features the platform does not accept yet: today the server's manifest validation refuses `kind` \"log\", `ttlSeconds`, `dailyWritesPerAuthor`, `write` \"anyone\" and `write` \"owner\" with a message saying they are not available yet, so those validate offline and are rejected at submit. Kept in lockstep with src/shared/constants/block-storage.constants.ts (a drift-guard test enforces equality); the imperative validator in that module is authoritative.",
381
+ "additionalProperties": false,
382
+ "required": ["collections"],
383
+ "properties": {
384
+ "collections": {
385
+ "type": "object",
386
+ "description": "Map of collection name → declaration. At least 1 and at most 16 collections. A collection name is lowercase snake_case: a letter, then up to 31 of a-z, 0-9 or _. Renaming a collection, or changing its visibility, is a NEW collection: the old one's documents are not carried over.",
387
+ "minProperties": 1,
388
+ "maxProperties": 16,
389
+ "additionalProperties": false,
390
+ "patternProperties": {
391
+ "^[a-z][a-z0-9_]{0,31}$": {
392
+ "type": "object",
393
+ "additionalProperties": false,
394
+ "required": ["visibility"],
395
+ "properties": {
396
+ "kind": {
397
+ "type": "string",
398
+ "description": "\"docs\" (the default): documents with app-chosen or server-assigned keys that can be updated in place. \"log\": append-only entries with server-assigned keys, never updated, trimmed to `maxEntries` per key space. RESERVED: civitai.com refuses \"log\" until log collections are enabled, so declare \"docs\" (or omit `kind`).",
399
+ "enum": ["docs", "log"]
400
+ },
401
+ "visibility": {
402
+ "type": "string",
403
+ "description": "Who can read the collection. \"private\": a separate key space per viewer — only that viewer reads or writes their documents. \"shared\": one key space for the whole app — every viewer of the app reads it, including signed-out viewers. Cannot be changed later: declare a new collection instead.",
404
+ "enum": ["private", "shared"]
405
+ },
406
+ "write": {
407
+ "type": "string",
408
+ "description": "Shared collections only, and REQUIRED on them. Who may create, update and delete documents. \"own\": eligible signed-in viewers create documents, and only a document's author updates or deletes it. \"anyone\": eligible signed-in viewers may update or delete any document — RESERVED: civitai.com refuses \"anyone\" until it is enabled. \"none\": nobody creates, updates or deletes documents — viewers may only increment the declared `counters` and set the declared `marks`, and incrementing a counter on a document that does not exist creates an empty one authored by no viewer; a \"none\" collection must declare at least one counter or mark. \"owner\": only the app owner writes documents; viewers can still mark and count. Only \"own\" and \"anyone\" require the `apps:storage:publish` scope. RESERVED: civitai.com refuses \"owner\" until owner-written documents are enabled. Available today: \"own\" and \"none\".",
409
+ "enum": ["own", "anyone", "owner", "none"]
410
+ },
411
+ "counters": {
412
+ "type": "array",
413
+ "description": "Shared collections only. Names of the counters each document carries — atomic tallies the app increments (for example plays). At most 8 distinct names, each lowercase snake_case: a letter, then up to 31 of a-z, 0-9 or _. A counter may not share a name with a mark on the same collection, or with the built-in `report` mark.",
414
+ "maxItems": 8,
415
+ "uniqueItems": true,
416
+ "items": {
417
+ "type": "string",
418
+ "pattern": "^[a-z][a-z0-9_]{0,31}$",
419
+ "not": { "enum": ["report"] }
420
+ }
421
+ },
422
+ "marks": {
423
+ "type": "array",
424
+ "description": "Shared collections only. Names of the per-user marks a viewer can set on a document (for example vote) — at most one of each per viewer per document. A reader sees each mark's count and whether THEY set it, never who else did. At most 8 distinct names, each lowercase snake_case: a letter, then up to 31 of a-z, 0-9 or _. `report` is built in on every shared collection and must NOT be declared. A mark may not share a name with a counter on the same collection.",
425
+ "maxItems": 8,
426
+ "uniqueItems": true,
427
+ "items": {
428
+ "type": "string",
429
+ "pattern": "^[a-z][a-z0-9_]{0,31}$",
430
+ "not": { "enum": ["report"] }
431
+ }
432
+ },
433
+ "indexes": {
434
+ "type": "object",
435
+ "description": "Fields of a document's `data` the app can filter and sort by, as a map of field PATH → type. A path is 1 to 3 segments joined by `.` (for example status, datasetId or meta.round), each a letter followed by up to 31 letters, digits or _; a field whose own name contains `.` cannot be indexed. At most 4 indexes. Types: \"string\" (values up to 128 bytes), \"number\", \"boolean\", \"time\". `key`, `createdAt` and `updatedAt` are built-in sort keys and cannot be index names. Changing an index's type is not supported: remove it and add one under a new name.",
436
+ "maxProperties": 4,
437
+ "additionalProperties": false,
438
+ "properties": {
439
+ "key": false,
440
+ "createdAt": false,
441
+ "updatedAt": false
442
+ },
443
+ "patternProperties": {
444
+ "^[A-Za-z][A-Za-z0-9_]{0,31}(\\.[A-Za-z][A-Za-z0-9_]{0,31}){0,2}$": {
445
+ "type": "string",
446
+ "enum": ["string", "number", "boolean", "time"]
447
+ }
448
+ }
449
+ },
450
+ "showAuthor": {
451
+ "type": "boolean",
452
+ "description": "Shared collections only. When true, readers see each viewer-authored document's author USERNAME. Default false: readers learn only whether they wrote a document themselves. The numeric user id is never exposed either way."
453
+ },
454
+ "ttlSeconds": {
455
+ "type": "integer",
456
+ "description": "RESERVED — civitai.com refuses this field until expiring documents are enabled, so do not declare it yet. Intended meaning: the default lifetime of a document in this collection, in seconds. The bounds (60 to 31536000, i.e. one minute to 365 days) are provisional and may change before the field is enabled.",
457
+ "minimum": 60,
458
+ "maximum": 31536000
459
+ },
460
+ "maxEntries": {
461
+ "type": "integer",
462
+ "description": "RESERVED — log collections are refused by civitai.com until enabled, so this field cannot be used yet. Intended meaning, for `kind` \"log\" only: the most entries kept per key space, the oldest trimmed beyond it, from 1 to 10000 (omitted means 10000). The default is provisional and may change before log collections are enabled.",
463
+ "minimum": 1,
464
+ "maximum": 10000
465
+ },
466
+ "dailyWritesPerAuthor": {
467
+ "type": "integer",
468
+ "description": "RESERVED — civitai.com refuses this field until per-collection write budgets are enabled, so do not declare it yet. Intended meaning: for a shared collection that viewers write (`write` \"own\" or \"anyone\"; refused with \"owner\" or \"none\"), the per-viewer daily budget for creating and updating documents in this collection in place of the platform default, from 1 to 1000, shown to the moderator at review. The bounds are provisional and may change before the field is enabled.",
469
+ "minimum": 1,
470
+ "maximum": 1000
471
+ }
472
+ },
473
+ "allOf": [
474
+ {
475
+ "$comment": "A shared collection must state its write policy; a private one carries none of the shared-only fields.",
476
+ "if": {
477
+ "required": ["visibility"],
478
+ "properties": { "visibility": { "const": "shared" } }
479
+ },
480
+ "then": { "required": ["write"] },
481
+ "else": {
482
+ "properties": {
483
+ "write": false,
484
+ "counters": false,
485
+ "marks": false,
486
+ "showAuthor": false,
487
+ "dailyWritesPerAuthor": false
488
+ }
489
+ }
490
+ },
491
+ {
492
+ "$comment": "maxEntries belongs to log collections only (kind defaults to docs).",
493
+ "if": {
494
+ "required": ["kind"],
495
+ "properties": { "kind": { "const": "log" } }
496
+ },
497
+ "else": { "properties": { "maxEntries": false } }
498
+ },
499
+ {
500
+ "$comment": "No viewer authors documents in a write:\"owner\" or write:\"none\" collection, so it has no per-author budget.",
501
+ "if": {
502
+ "required": ["write"],
503
+ "properties": { "write": { "enum": ["owner", "none"] } }
504
+ },
505
+ "then": { "properties": { "dailyWritesPerAuthor": false } }
506
+ },
507
+ {
508
+ "$comment": "A write:\"none\" collection is written only through counters and marks, so it must declare at least one.",
509
+ "if": {
510
+ "required": ["write"],
511
+ "properties": { "write": { "const": "none" } }
512
+ },
513
+ "then": {
514
+ "anyOf": [
515
+ { "required": ["counters"], "properties": { "counters": { "minItems": 1 } } },
516
+ { "required": ["marks"], "properties": { "marks": { "minItems": 1 } } }
517
+ ]
518
+ }
519
+ }
520
+ ]
521
+ }
522
+ }
523
+ }
524
+ }
525
+ },
376
526
  "targets": {
377
527
  "type": "array",
378
528
  "description": "Model-page slot targets. Each target's slotId must be a known registered model slot (not the page slot). Optional for page-only apps.",