@civitai/app-sdk 0.51.2 → 0.52.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.
@@ -77,5 +77,5 @@ export { isModelSlotContext, isPageSlotContext } from './types.js';
77
77
  * in `types.ts` for which of the two it uses and why.
78
78
  */
79
79
  export { isSignedIn } from './types.js';
80
- export type { BlockContext, KnownSlotId, ModelSlotId, PageSlotContext, PageSlotId, UnknownSlotContext, BlockManifest, 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, BlockTextToImageParams, BlockWorkflowSnapshot, 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, } from './types.js';
80
+ export type { BlockContext, KnownSlotId, ModelSlotId, PageSlotContext, PageSlotId, UnknownSlotContext, 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, BlockTextToImageParams, BlockWorkflowSnapshot, 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, } from './types.js';
81
81
  //# sourceMappingURL=index.d.ts.map
@@ -24,6 +24,8 @@ export declare const BLOCK_SCOPES: {
24
24
  readonly COLLECTIONS_WRITE_SELF: "collections:write:self";
25
25
  readonly COLLECTIONS_READ_PRIVATE: "collections:read:private";
26
26
  readonly POSTS_WRITE_SELF: "posts:write:self";
27
+ readonly GOODS_READ_SELF: "goods:read:self";
28
+ readonly GOODS_PURCHASE_SELF: "goods:purchase:self";
27
29
  };
28
30
  export type BlockScopeKey = keyof typeof BLOCK_SCOPES;
29
31
  export type BlockScope = (typeof BLOCK_SCOPES)[BlockScopeKey];
@@ -48,6 +48,20 @@ export const BLOCK_SCOPES = {
48
48
  // per-post confirm rendering the SERVER'S resolution of the request, and the
49
49
  // server re-runs every guard.
50
50
  POSTS_WRITE_SELF: 'posts:write:self',
51
+ // goods:* — the DIGITAL GOODS rail (civitai/civitai#5171): the platform sells
52
+ // a manifest-declared entitlement to the viewer for Buzz, on the app's behalf.
53
+ // These were live on the SERVER and in the canonical schema while absent here,
54
+ // so `defineBlock` rejected both and no app scaffolded from this repo could
55
+ // DECLARE them — the hooks that call them were unreachable. Found by the
56
+ // round-0 reachability question on app-starters#489.
57
+ //
58
+ // `goods:read:self` is CONSENT-EXEMPT by design: the read is scoped
59
+ // server-side to the calling app's own appBlockId, so it can only ever return
60
+ // what that app itself sold and there is no third-party data to consent to.
61
+ // `goods:purchase:self` is CONSENT-GATED — money out of the viewer's balance
62
+ // always needs an explicit grant. Do not collapse the two.
63
+ GOODS_READ_SELF: 'goods:read:self',
64
+ GOODS_PURCHASE_SELF: 'goods:purchase:self',
51
65
  };
52
66
  /**
53
67
  * Format helper for the block-scope shape — 3 OR 4 colon-separated lowercase
@@ -1402,6 +1402,40 @@ export interface ManifestPreview {
1402
1402
  description: string;
1403
1403
  screenshots?: string[];
1404
1404
  }
1405
+ /**
1406
+ * One entry of a manifest's `goods[]`. Mirrors the canonical schema exactly;
1407
+ * `id`, `title` and `priceBuzz` are required there and so are they here.
1408
+ *
1409
+ * 🔴 DECLARED BEFORE `BlockManifestV1`'S DOCBLOCK ON PURPOSE. Inserting an
1410
+ * interface BETWEEN a docblock and the declaration it documents does not just
1411
+ * look untidy — `tsc` emits both comments onto THIS interface in
1412
+ * `dist/blocks/types.d.ts` and leaves `BlockManifestV1` undocumented, so the
1413
+ * published types told a reader that "only `blockId`, `version`, `name`,
1414
+ * `contentRating` and `scopes` are required" about a type with three fields and
1415
+ * different requirements. Keep any new sibling above this comment or below
1416
+ * `BlockManifestV1`.
1417
+ */
1418
+ export interface BlockManifestGood {
1419
+ /** Lowercase alphanumeric with `-`/`_`, at most 64 chars. Colon-free, because it is composed into a redis key and a ledger external id. */
1420
+ id: string;
1421
+ /** At most 80 chars. */
1422
+ title: string;
1423
+ /** At most 500 chars. */
1424
+ description?: string;
1425
+ /**
1426
+ * Whole Buzz, minimum 2 — at a price of 1 the owner's floored 70% share is
1427
+ * ZERO, so the app would sell an item and earn nothing from it, permanently.
1428
+ */
1429
+ priceBuzz: number;
1430
+ /**
1431
+ * What the entitlement grants. `app_unlock` marks a one-time unlock of the app
1432
+ * itself; it is RECORDED identically today and the platform does not yet act
1433
+ * on it, so declaring it buys nothing unless you intend that later behaviour.
1434
+ */
1435
+ kind?: 'good' | 'app_unlock';
1436
+ /** Opaque app payload, carried verbatim onto the entitlement. Never interpreted by the platform. */
1437
+ payload?: Record<string, unknown>;
1438
+ }
1405
1439
  /**
1406
1440
  * v1 manifest shape. Mirrors `schemas/app-block/v1.json` — keep them in sync.
1407
1441
  *
@@ -1452,6 +1486,23 @@ export interface BlockManifestV1 {
1452
1486
  * with the canonical schema's `scopeJustifications` (civitai #3195).
1453
1487
  */
1454
1488
  scopeJustifications?: Record<string, string>;
1489
+ /**
1490
+ * Optional DIGITAL GOODS catalog — entitlements the platform sells to a viewer
1491
+ * for Buzz on this app's behalf. Manifest-governed and REVIEW-GATED: the
1492
+ * catalog a moderator approves is the catalog that can be sold, and changing a
1493
+ * price means shipping a new version and being re-reviewed.
1494
+ *
1495
+ * Declaring goods is not by itself permission to sell — the manifest must also
1496
+ * carry `goods:purchase:self` in `scopes`.
1497
+ *
1498
+ * 🔴 THE TYPE WAS ABSENT WHILE THE SCHEMA PROPERTY EXISTED, so `defineBlock`'s
1499
+ * documented inline-literal form rejected a `goods` declaration with TS2353
1500
+ * even though Ajv accepted the same manifest from a JSON file. Nothing could
1501
+ * catch that: there is a vendored-schema↔`BLOCK_SCOPES` cross-check but no
1502
+ * schema-properties↔interface-keys check, and the repo's manifest tests cast.
1503
+ * Kept in lockstep with the canonical schema's `goods`.
1504
+ */
1505
+ goods?: BlockManifestGood[];
1455
1506
  /** Optional; the canonical declares no required sub-field. */
1456
1507
  iframe?: ManifestIframe;
1457
1508
  /** Full-page surface descriptor (W10). */
@@ -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 { BlockManifest, BlockManifestV1 } from '../blocks/types.js';
18
+ export type { BlockManifest, BlockManifestGood, BlockManifestV1 } 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.51.2",
3
+ "version": "0.52.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",
@@ -99,13 +99,15 @@
99
99
  "collections:read:self",
100
100
  "collections:write:self",
101
101
  "collections:read:private",
102
- "posts:write:self"
102
+ "posts:write:self",
103
+ "goods:read:self",
104
+ "goods:purchase:self"
103
105
  ]
104
106
  }
105
107
  },
106
108
  "scopeJustifications": {
107
109
  "type": "object",
108
- "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`, `buzz:read:self`, `collections:read:private`, `apps:storage:shared:write`, `posts:write:self`) 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.",
110
+ "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`) 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.",
109
111
  "additionalProperties": {
110
112
  "type": "string",
111
113
  "minLength": 1,
@@ -221,6 +223,51 @@
221
223
  }
222
224
  }
223
225
  },
226
+ "goods": {
227
+ "type": "array",
228
+ "description": "Optional DIGITAL GOODS catalog — entitlements the platform sells to a viewer on your app's behalf, for Buzz. Manifest-governed and REVIEW-GATED: the catalog a moderator approves is the catalog that can be sold, and changing a price means shipping a new version and being re-reviewed. The platform owns the ledger (who bought what, when, at what price, and its refund state); the good's MEANING is your app's business — read the viewer's entitlements from GET /api/v1/blocks/entitlements and keep the semantics in your own app storage. Declaring goods does not by itself let you sell: the app must also declare the `goods:purchase:self` scope (and `goods:read:self` to read entitlements back), and the viewer must consent. Sales split platform 30% / app owner 70%, paid immediately. Kept in lockstep with the bounds in src/shared/constants/block-goods.constants.ts (a drift-guard test enforces equality); the imperative validator in that same module is authoritative.",
229
+ "maxItems": 32,
230
+ "items": {
231
+ "type": "object",
232
+ "additionalProperties": false,
233
+ "required": ["id", "title", "priceBuzz"],
234
+ "properties": {
235
+ "id": {
236
+ "type": "string",
237
+ "description": "Stable identifier for this good, unique within the manifest. This is what a purchase and an entitlement are keyed by, so changing it in a later version orphans every entitlement already granted under the old id. Lowercase letters, digits, - and _, starting with a letter or digit.",
238
+ "minLength": 1,
239
+ "maxLength": 64,
240
+ "pattern": "^[a-z0-9][a-z0-9_-]*$"
241
+ },
242
+ "title": {
243
+ "type": "string",
244
+ "description": "Human-readable name shown to the viewer at purchase. Non-empty; the server measures the TRIMMED length, so this maxLength is never more permissive than the server.",
245
+ "minLength": 1,
246
+ "maxLength": 80
247
+ },
248
+ "description": {
249
+ "type": "string",
250
+ "description": "Optional one-paragraph explanation of what the viewer gets.",
251
+ "maxLength": 500
252
+ },
253
+ "priceBuzz": {
254
+ "type": "integer",
255
+ "description": "Price in whole Buzz. Buzz has no sub-unit, so this must be a whole number, and the minimum is 2 rather than 1 because the app owner's 70% share is floored — a 1 Buzz item would earn its owner nothing, permanently. Capped so a single purchase can never drain an account; a viewer also has a daily ceiling across every app they have installed, so a purchase can be refused with a clean 4xx even at a legal price. Both bounds are kept in lockstep with BLOCK_GOOD_MIN_PRICE_BUZZ / BLOCK_GOOD_MAX_PRICE_BUZZ in src/shared/constants/block-goods.constants.ts by a drift-guard test.",
256
+ "minimum": 2,
257
+ "maximum": 50000
258
+ },
259
+ "kind": {
260
+ "type": "string",
261
+ "description": "What the entitlement grants, from the platform's point of view. \"good\" (the default) is an ordinary in-app purchase the platform records and does not interpret. \"app_unlock\" marks a one-time unlock of the app itself; it is RECORDED IDENTICALLY today and the platform does NOT yet act on it — the paid-app access gate is a later change that reads this value. Declaring it now buys nothing, so leave it unset unless you intend that later behaviour.",
262
+ "enum": ["good", "app_unlock"]
263
+ },
264
+ "payload": {
265
+ "type": "object",
266
+ "description": "Optional OPAQUE payload copied verbatim onto the entitlement at purchase and handed back to your app unchanged. The platform never reads or interprets it. It is manifest-sourced rather than client-supplied precisely so that what an entitlement carries is something a moderator saw. Must serialize to at most 2048 bytes."
267
+ }
268
+ }
269
+ }
270
+ },
224
271
  "targets": {
225
272
  "type": "array",
226
273
  "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.",