@civitai/app-sdk 0.72.0 → 0.74.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.
@@ -28,6 +28,13 @@ export type { BlockScope, BlockScopeKey, BlockCategory } from './scopes.js';
28
28
  export { BrowsingLevel, SFW_LEVELS, NSFW_LEVELS, isSfwCeiling, isLevelAllowed, effectiveBrowsingCeiling, } from './browsingLevel.js';
29
29
  export type { BrowsingLevelKey, BrowsingLevelBit, ColorDomain } from './browsingLevel.js';
30
30
  export { APP_STORAGE_MAX_VALUE_BYTES, APP_STORAGE_MAX_BYTES, APP_STORAGE_MAX_ROWS, } from './appStorageLimits.js';
31
+ /**
32
+ * App reactions (`SET_ENTITY_REACTION` / `GET_ENTITY_REACTIONS`, scope
33
+ * `social:react:self`). The vocabulary mirrors civitai's
34
+ * `block-reaction.constants.ts`; see `reactions.ts`.
35
+ */
36
+ export { APP_REACTIONS, STORED_REACTIONS, REACTION_ENTITY_TYPES, REACTION_READ_MAX_IDS, REACT_TICKET_MAX_LENGTH, ENTITY_REACTION_ERROR_CODES, isEntityReactionErrorCode, isAppReaction, } from './reactions.js';
37
+ export type { BlockAppReaction, BlockStoredReaction, BlockReactionEntityType, BlockEntityReactionErrorCode, BlockReactionCounts, BlockEntityReactionState, BlockEntityReactionResult, BlockEntityReactionsResult, } from './reactions.js';
31
38
  export { BLOCK_IDEMPOTENCY_KEY_REGEX, BLOCK_IDEMPOTENCY_KEY_MAX_LENGTH, isValidBlockIdempotencyKey, blockIdempotencyKeyRejection, } from './idempotency.js';
32
39
  /**
33
40
  * The `srcdoc` escape hatch for embedding one of your own bundled documents
@@ -31,6 +31,12 @@ export { BlockManifestError } from './manifestError.js';
31
31
  export { BLOCK_SCOPES, BLOCK_SCOPE_PATTERN, BLOCK_CATEGORIES, BLOCK_TAGLINE_MAX_LENGTH, } from './scopes.js';
32
32
  export { BrowsingLevel, SFW_LEVELS, NSFW_LEVELS, isSfwCeiling, isLevelAllowed, effectiveBrowsingCeiling, } from './browsingLevel.js';
33
33
  export { APP_STORAGE_MAX_VALUE_BYTES, APP_STORAGE_MAX_BYTES, APP_STORAGE_MAX_ROWS, } from './appStorageLimits.js';
34
+ /**
35
+ * App reactions (`SET_ENTITY_REACTION` / `GET_ENTITY_REACTIONS`, scope
36
+ * `social:react:self`). The vocabulary mirrors civitai's
37
+ * `block-reaction.constants.ts`; see `reactions.ts`.
38
+ */
39
+ export { APP_REACTIONS, STORED_REACTIONS, REACTION_ENTITY_TYPES, REACTION_READ_MAX_IDS, REACT_TICKET_MAX_LENGTH, ENTITY_REACTION_ERROR_CODES, isEntityReactionErrorCode, isAppReaction, } from './reactions.js';
34
40
  export { BLOCK_IDEMPOTENCY_KEY_REGEX, BLOCK_IDEMPOTENCY_KEY_MAX_LENGTH, isValidBlockIdempotencyKey, blockIdempotencyKeyRejection, } from './idempotency.js';
35
41
  /**
36
42
  * The `srcdoc` escape hatch for embedding one of your own bundled documents
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import type { ColorDomain } from './browsingLevel.js';
11
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';
12
+ import type { BlockAppReaction, BlockEntityReactionErrorCode, BlockEntityReactionResult, BlockEntityReactionsResult, BlockReactionEntityType } from './reactions.js';
12
13
  /**
13
14
  * Filter params for `GET_BUZZ_TRANSACTIONS`. All optional; the host validates
14
15
  * them server-side (they are NEVER trusted for auth — the account is self-bound
@@ -531,6 +532,20 @@ export type ParentToBlockMessage = {
531
532
  result?: BlockCollectionFollowResult;
532
533
  error?: BlockCollectionFollowErrorCode | string;
533
534
  };
535
+ } | {
536
+ type: 'ENTITY_REACTION_RESULT';
537
+ payload: {
538
+ requestId: string;
539
+ result?: BlockEntityReactionResult;
540
+ error?: BlockEntityReactionErrorCode | string;
541
+ };
542
+ } | {
543
+ type: 'ENTITY_REACTIONS_RESULT';
544
+ payload: {
545
+ requestId: string;
546
+ result?: BlockEntityReactionsResult;
547
+ error?: BlockEntityReactionErrorCode | string;
548
+ };
534
549
  } | {
535
550
  type: 'CANCEL_APP_WORKFLOW_RESULT';
536
551
  payload: {
@@ -1112,6 +1127,30 @@ export type BlockToParentMessage = {
1112
1127
  /** `true` ⇒ follow, `false` ⇒ unfollow. */
1113
1128
  follow: boolean;
1114
1129
  };
1130
+ } | {
1131
+ type: 'SET_ENTITY_REACTION';
1132
+ payload: {
1133
+ requestId: string;
1134
+ entityType: BlockReactionEntityType;
1135
+ /** Positive integer. A numeric STRING is refused, never coerced. */
1136
+ entityId: number;
1137
+ /** One of `APP_REACTIONS`. `Dislike` is refused as `invalid-request`. */
1138
+ reaction: BlockAppReaction;
1139
+ /** `true` ⇒ the reaction exists afterwards; `false` ⇒ it does not. */
1140
+ reacted: boolean;
1141
+ /** 1 to `REACT_TICKET_MAX_LENGTH` characters. */
1142
+ ticket?: string;
1143
+ };
1144
+ } | {
1145
+ type: 'GET_ENTITY_REACTIONS';
1146
+ payload: {
1147
+ requestId: string;
1148
+ entityType: BlockReactionEntityType;
1149
+ /** 1 to `REACTION_READ_MAX_IDS` positive integers. Duplicates are collapsed. */
1150
+ ids: number[];
1151
+ /** A ticket per id, keyed by the id as a string. */
1152
+ tickets?: Record<string, string>;
1153
+ };
1115
1154
  };
1116
1155
  export type BlockToParentMessageType = BlockToParentMessage['type'];
1117
1156
  /**
@@ -1155,7 +1194,7 @@ export type BlockToParentMessageType = BlockToParentMessage['type'];
1155
1194
  * it is a measurement, not an invariant, and civitai's own compile-time gate
1156
1195
  * (published SDK union ⊆ `keyof INVENTORY`) is what holds one direction of it.
1157
1196
  */
1158
- 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"];
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"];
1159
1198
  /**
1160
1199
  * The bucket a message type falls into when the protocol does not declare it.
1161
1200
  *
@@ -101,6 +101,8 @@ export const BLOCK_TO_PARENT_MESSAGE_TYPES = [
101
101
  'SHARED_REPORT',
102
102
  'SAVE_IMAGE',
103
103
  'SET_COLLECTION_FOLLOW',
104
+ 'SET_ENTITY_REACTION',
105
+ 'GET_ENTITY_REACTIONS',
104
106
  ];
105
107
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
106
108
  const _messageTypesGate = true;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * App reactions — the block side of `SET_ENTITY_REACTION` / `GET_ENTITY_REACTIONS`
3
+ * (scope `social:react:self`, civitai/civitai#6144).
4
+ *
5
+ * A block may set, or read, the VIEWER'S reaction on an image a block endpoint
6
+ * served it. The host calls `blocks.setReactionFromApp` /
7
+ * `blocks.getReactionsForApp`, which run the SAME handler as the site's own
8
+ * reaction button: rewards, metrics and notifications are the site's. What the
9
+ * app path adds is an audit row per change, a provenance ticket per image and a
10
+ * fail-closed kill switch.
11
+ *
12
+ * Every constant here MIRRORS civitai/civitai's
13
+ * `src/shared/constants/block-reaction.constants.ts`. The host's copy is the
14
+ * definition and this is the copy; `test/blocks/reactions.test.ts` pins each
15
+ * value literally, so a change on either side is a visible diff here.
16
+ *
17
+ * The scope is `BLOCK_SCOPES.SOCIAL_REACT_SELF`: consent-gated (a token carries
18
+ * it only after the viewer granted it) and sensitive (the manifest must justify
19
+ * it in `scopeJustifications`).
20
+ *
21
+ * Zero dependencies, like the rest of `./blocks`.
22
+ */
23
+ /**
24
+ * The reactions an app may SET on an image.
25
+ *
26
+ * 🔴 `Dislike` IS DELIBERATELY ABSENT, and the host and the server both refuse
27
+ * it. Image stats hardcode the dislike count to 0, so a Dislike is invisible on
28
+ * the site, yet it is stored and still pays the reactor's encouragement reward.
29
+ */
30
+ export declare const APP_REACTIONS: readonly ["Like", "Heart", "Laugh", "Cry"];
31
+ export type BlockAppReaction = (typeof APP_REACTIONS)[number];
32
+ /**
33
+ * Every reaction the site STORES. A viewer's existing reactions are reported in
34
+ * this wider set: a viewer who disliked an image on the site still holds that
35
+ * Dislike, and the read says so, even though an app may not set one.
36
+ */
37
+ export declare const STORED_REACTIONS: readonly ["Like", "Dislike", "Laugh", "Cry", "Heart"];
38
+ export type BlockStoredReaction = (typeof STORED_REACTIONS)[number];
39
+ /** Entity types the host accepts. Image only in v1: nothing else is ticketed yet. */
40
+ export declare const REACTION_ENTITY_TYPES: readonly ["image"];
41
+ export type BlockReactionEntityType = (typeof REACTION_ENTITY_TYPES)[number];
42
+ /** Most ids one `GET_ENTITY_REACTIONS` may carry. More is refused as `invalid-request`. */
43
+ export declare const REACTION_READ_MAX_IDS = 100;
44
+ /** Longest ticket the host accepts. A real one is about 60 characters. */
45
+ export declare const REACT_TICKET_MAX_LENGTH = 256;
46
+ /**
47
+ * The CLOSED set of errors `ENTITY_REACTION_RESULT` and `ENTITY_REACTIONS_RESULT`
48
+ * carry. Unlike the follow and post bridges, the host maps EVERY failure,
49
+ * its own refusals and every server error alike, onto one of these
50
+ * (`classifyEntityReactionError` in civitai's `reactionGate.ts`), so a block can
51
+ * branch on the code.
52
+ *
53
+ * - `invalid-request`: a malformed payload, or a reaction apps may not set
54
+ * (`Dislike`). A block bug, not a viewer outcome.
55
+ * - `sign-in-required`: no session. Route it into `useRequestSignIn()`.
56
+ * - `review-mode`: the app is running in a moderator's review sandbox, where
57
+ * the scope is never granted.
58
+ * - `not-ready`: the host has not finished the handshake, or holds no token yet.
59
+ * Retry after `ready`.
60
+ * - `needs-consent`: the viewer has not granted `social:react:self`. Ask with
61
+ * `useRequestConsent()`.
62
+ * - `entity-unavailable`: everything else. A missing, expired or foreign
63
+ * ticket, an image the viewer may not react to, an owner who blocked the
64
+ * viewer, the kill switch, or a network failure. Deliberately one code: the
65
+ * block can retry or give up, and there is no truthful finer answer.
66
+ * - `rate-limited`: the viewer's reaction quota is spent. It is the SAME quota
67
+ * the site's own reaction button draws on.
68
+ */
69
+ export declare const ENTITY_REACTION_ERROR_CODES: readonly ["invalid-request", "sign-in-required", "review-mode", "not-ready", "needs-consent", "entity-unavailable", "rate-limited"];
70
+ export type BlockEntityReactionErrorCode = (typeof ENTITY_REACTION_ERROR_CODES)[number];
71
+ /** `true` when `value` is one of the closed {@link ENTITY_REACTION_ERROR_CODES}. */
72
+ export declare function isEntityReactionErrorCode(value: unknown): value is BlockEntityReactionErrorCode;
73
+ /** `true` when `value` is a reaction an app may set ({@link APP_REACTIONS}). */
74
+ export declare function isAppReaction(value: unknown): value is BlockAppReaction;
75
+ /** Reaction counts for one image, as the site's own feeds show them. */
76
+ export type BlockReactionCounts = Record<BlockAppReaction, number>;
77
+ /** One image's reaction state, as the read reports it. */
78
+ export interface BlockEntityReactionState {
79
+ counts: BlockReactionCounts;
80
+ /** Every reaction the viewer holds on the image. A viewer may hold several. */
81
+ viewerReactions: BlockStoredReaction[];
82
+ /**
83
+ * `true` when the metric store could not answer for this image. `counts` are
84
+ * then zeros, and must not be shown as "nobody reacted".
85
+ */
86
+ statsUnknown: boolean;
87
+ }
88
+ /** The success payload of `ENTITY_REACTION_RESULT`. */
89
+ export interface BlockEntityReactionResult extends BlockEntityReactionState {
90
+ entityType: BlockReactionEntityType;
91
+ entityId: number;
92
+ reaction: BlockAppReaction;
93
+ /**
94
+ * Whether the viewer holds `reaction` NOW, read on the primary after the
95
+ * write. 🔴 When this differs from the `reacted` the block asked for, a
96
+ * concurrent change (a double tap, the viewer's own click elsewhere) won and
97
+ * this request did not apply.
98
+ */
99
+ reacted: boolean;
100
+ /**
101
+ * Did THIS request write a change? `false` when the viewer was already in the
102
+ * requested state: nothing was toggled, so nothing was paid and no metric
103
+ * moved.
104
+ */
105
+ changed: boolean;
106
+ /**
107
+ * 🔴 `counts` CAN LAG `reacted`. They come from the metric store, which
108
+ * updates asynchronously, so right after a change they may not include it yet.
109
+ * `reacted` and `viewerReactions` are the truth.
110
+ */
111
+ counts: BlockReactionCounts;
112
+ }
113
+ /** The success payload of `ENTITY_REACTIONS_RESULT`. */
114
+ export interface BlockEntityReactionsResult {
115
+ entityType: BlockReactionEntityType;
116
+ /** One entry per ticketed id, keyed by the id as a string (it crossed JSON). */
117
+ byId: Record<string, BlockEntityReactionState>;
118
+ /**
119
+ * Ids the host did not answer, because their ticket was missing, expired, or
120
+ * issued to another app or viewer. 🔴 Not "the viewer has not reacted": an
121
+ * unanswered id has NO known state, and a block must not offer a fresh
122
+ * reaction on the assumption that there is none.
123
+ */
124
+ unavailable: number[];
125
+ }
126
+ //# sourceMappingURL=reactions.d.ts.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * App reactions — the block side of `SET_ENTITY_REACTION` / `GET_ENTITY_REACTIONS`
3
+ * (scope `social:react:self`, civitai/civitai#6144).
4
+ *
5
+ * A block may set, or read, the VIEWER'S reaction on an image a block endpoint
6
+ * served it. The host calls `blocks.setReactionFromApp` /
7
+ * `blocks.getReactionsForApp`, which run the SAME handler as the site's own
8
+ * reaction button: rewards, metrics and notifications are the site's. What the
9
+ * app path adds is an audit row per change, a provenance ticket per image and a
10
+ * fail-closed kill switch.
11
+ *
12
+ * Every constant here MIRRORS civitai/civitai's
13
+ * `src/shared/constants/block-reaction.constants.ts`. The host's copy is the
14
+ * definition and this is the copy; `test/blocks/reactions.test.ts` pins each
15
+ * value literally, so a change on either side is a visible diff here.
16
+ *
17
+ * The scope is `BLOCK_SCOPES.SOCIAL_REACT_SELF`: consent-gated (a token carries
18
+ * it only after the viewer granted it) and sensitive (the manifest must justify
19
+ * it in `scopeJustifications`).
20
+ *
21
+ * Zero dependencies, like the rest of `./blocks`.
22
+ */
23
+ /**
24
+ * The reactions an app may SET on an image.
25
+ *
26
+ * 🔴 `Dislike` IS DELIBERATELY ABSENT, and the host and the server both refuse
27
+ * it. Image stats hardcode the dislike count to 0, so a Dislike is invisible on
28
+ * the site, yet it is stored and still pays the reactor's encouragement reward.
29
+ */
30
+ export const APP_REACTIONS = ['Like', 'Heart', 'Laugh', 'Cry'];
31
+ /**
32
+ * Every reaction the site STORES. A viewer's existing reactions are reported in
33
+ * this wider set: a viewer who disliked an image on the site still holds that
34
+ * Dislike, and the read says so, even though an app may not set one.
35
+ */
36
+ export const STORED_REACTIONS = ['Like', 'Dislike', 'Laugh', 'Cry', 'Heart'];
37
+ /** Entity types the host accepts. Image only in v1: nothing else is ticketed yet. */
38
+ export const REACTION_ENTITY_TYPES = ['image'];
39
+ /** Most ids one `GET_ENTITY_REACTIONS` may carry. More is refused as `invalid-request`. */
40
+ export const REACTION_READ_MAX_IDS = 100;
41
+ /** Longest ticket the host accepts. A real one is about 60 characters. */
42
+ export const REACT_TICKET_MAX_LENGTH = 256;
43
+ /**
44
+ * The CLOSED set of errors `ENTITY_REACTION_RESULT` and `ENTITY_REACTIONS_RESULT`
45
+ * carry. Unlike the follow and post bridges, the host maps EVERY failure,
46
+ * its own refusals and every server error alike, onto one of these
47
+ * (`classifyEntityReactionError` in civitai's `reactionGate.ts`), so a block can
48
+ * branch on the code.
49
+ *
50
+ * - `invalid-request`: a malformed payload, or a reaction apps may not set
51
+ * (`Dislike`). A block bug, not a viewer outcome.
52
+ * - `sign-in-required`: no session. Route it into `useRequestSignIn()`.
53
+ * - `review-mode`: the app is running in a moderator's review sandbox, where
54
+ * the scope is never granted.
55
+ * - `not-ready`: the host has not finished the handshake, or holds no token yet.
56
+ * Retry after `ready`.
57
+ * - `needs-consent`: the viewer has not granted `social:react:self`. Ask with
58
+ * `useRequestConsent()`.
59
+ * - `entity-unavailable`: everything else. A missing, expired or foreign
60
+ * ticket, an image the viewer may not react to, an owner who blocked the
61
+ * viewer, the kill switch, or a network failure. Deliberately one code: the
62
+ * block can retry or give up, and there is no truthful finer answer.
63
+ * - `rate-limited`: the viewer's reaction quota is spent. It is the SAME quota
64
+ * the site's own reaction button draws on.
65
+ */
66
+ export const ENTITY_REACTION_ERROR_CODES = [
67
+ 'invalid-request',
68
+ 'sign-in-required',
69
+ 'review-mode',
70
+ 'not-ready',
71
+ 'needs-consent',
72
+ 'entity-unavailable',
73
+ 'rate-limited',
74
+ ];
75
+ const ERROR_CODE_SET = new Set(ENTITY_REACTION_ERROR_CODES);
76
+ const APP_REACTION_SET = new Set(APP_REACTIONS);
77
+ /** `true` when `value` is one of the closed {@link ENTITY_REACTION_ERROR_CODES}. */
78
+ export function isEntityReactionErrorCode(value) {
79
+ return typeof value === 'string' && ERROR_CODE_SET.has(value);
80
+ }
81
+ /** `true` when `value` is a reaction an app may set ({@link APP_REACTIONS}). */
82
+ export function isAppReaction(value) {
83
+ return typeof value === 'string' && APP_REACTION_SET.has(value);
84
+ }
85
+ //# sourceMappingURL=reactions.js.map
@@ -29,6 +29,7 @@ export declare const BLOCK_SCOPES: {
29
29
  readonly GOODS_READ_SELF: "goods:read:self";
30
30
  readonly GOODS_PURCHASE_SELF: "goods:purchase:self";
31
31
  readonly APPS_STORE_ITEMS_WRITE: "apps:store:items:write";
32
+ readonly SOCIAL_REACT_SELF: "social:react:self";
32
33
  };
33
34
  export type BlockScopeKey = keyof typeof BLOCK_SCOPES;
34
35
  export type BlockScope = (typeof BLOCK_SCOPES)[BlockScopeKey];
@@ -90,6 +90,14 @@ export const BLOCK_SCOPES = {
90
90
  // Requires a signed-in (non-anonymous) subject. 4-segment, like the shared
91
91
  // storage scopes — see BLOCK_SCOPE_PATTERN below.
92
92
  APPS_STORE_ITEMS_WRITE: 'apps:store:items:write',
93
+ // social:react:self — set the VIEWER'S reaction on an image a block endpoint
94
+ // served the app (civitai/civitai#6144). SENSITIVE (a public, reward-paying
95
+ // reaction under the viewer's name, so a declaring manifest MUST carry a
96
+ // `scopeJustifications` entry) and CONSENT-GATED. The bridge is
97
+ // SET_ENTITY_REACTION / GET_ENTITY_REACTIONS (see `reactions.ts`), and
98
+ // `useReactions` in `@civitai/blocks-react` drives it. The server gates it
99
+ // behind a fail-closed flag as well as the grant.
100
+ SOCIAL_REACT_SELF: 'social:react:self',
93
101
  };
94
102
  /**
95
103
  * Format helper for the block-scope shape — 2, 3 OR 4 colon-separated
@@ -2573,7 +2573,8 @@ export interface BlockStorageIndexes {
2573
2573
  /**
2574
2574
  * A PRIVATE collection — a separate key space per viewer, read and written
2575
2575
  * only by that viewer. Carries none of the shared-only fields (`write`,
2576
- * `counters`, `marks`, `showAuthor`, `dailyWritesPerAuthor`); the canonical
2576
+ * `counters`, `marks`, `showAuthor`, `dailyWritesPerAuthor`,
2577
+ * `ownedModelVersions`); the canonical
2577
2578
  * forbids them here. See {@link BlockStorageSharedCollectionDeclaration} for
2578
2579
  * the fields both kinds share.
2579
2580
  */
@@ -2656,6 +2657,32 @@ export interface BlockStorageSharedCollectionDeclaration {
2656
2657
  * collection viewers write (`write` `'own'` or `'anyone'` only).
2657
2658
  */
2658
2659
  dailyWritesPerAuthor?: number;
2660
+ /**
2661
+ * `write` `'own'` collections only — the canonical refuses it under
2662
+ * `'anyone'`, `'owner'` and `'none'`, and on private collections. Fields of a
2663
+ * document's `data` that, when present, must hold the id of a Civitai model
2664
+ * version owned by the viewer writing the document — for example
2665
+ * `['/workflow/modelVersionId']` for a collection where viewers publish their
2666
+ * own workflows.
2667
+ *
2668
+ * 1 to 4 distinct pointers, each 1 to 4 segments of `/` then a letter or `_`
2669
+ * followed by up to 63 letters, digits or `_` (pattern
2670
+ * `^(/[A-Za-z_][A-Za-z0-9_]{0,63}){1,4}$`). A pointer names one field of
2671
+ * nested objects: it never indexes into an array, and a step through an
2672
+ * array or any other non-object finds nothing.
2673
+ *
2674
+ * On every create and update the server resolves each pointer in the new
2675
+ * `data`. An absent field is allowed. A present one must be a positive
2676
+ * integer naming a model version whose model the writer owns; anything else
2677
+ * — someone else's version, a missing or deleted one, a value that is not a
2678
+ * positive integer — is refused 403 `resource_not_owned`, with
2679
+ * `details.pointer` naming the field, before moderation and before any
2680
+ * write budget is spent. If the ownership lookup itself fails, the write is
2681
+ * refused 503 `service_unavailable` (retryable). Ownership is checked only
2682
+ * when a document is written: documents already stored keep the id if the
2683
+ * model later changes hands or is deleted.
2684
+ */
2685
+ ownedModelVersions?: string[];
2659
2686
  }
2660
2687
  /**
2661
2688
  * One collection declaration — see {@link BlockManifestV1.storage}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/app-sdk",
3
- "version": "0.72.0",
3
+ "version": "0.74.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",
@@ -104,13 +104,14 @@
104
104
  "posts:write:self",
105
105
  "goods:read:self",
106
106
  "goods:purchase:self",
107
- "apps:store:items:write"
107
+ "apps:store:items:write",
108
+ "social:react:self"
108
109
  ]
109
110
  }
110
111
  },
111
112
  "scopeJustifications": {
112
113
  "type": "object",
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.",
114
+ "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`, `social:react: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.",
114
115
  "additionalProperties": {
115
116
  "type": "string",
116
117
  "minLength": 1,
@@ -468,6 +469,17 @@
468
469
  "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
470
  "minimum": 1,
470
471
  "maximum": 1000
472
+ },
473
+ "ownedModelVersions": {
474
+ "type": "array",
475
+ "description": "Shared collections with `write` \"own\" only. Fields of a document's `data` that, when present, must hold a Civitai model version id owned by the viewer writing the document — for example [\"/workflow/modelVersionId\"] for a collection where viewers publish their own workflows. Each entry is a pointer of 1 to 4 segments, each \"/\" then a letter or _ followed by up to 63 letters, digits or _; 1 to 4 distinct pointers. A pointer names one field of nested objects: it never indexes into an array, and a step through an array or any other non-object finds nothing. On every create and update the server resolves each pointer in the new `data`: a field that is absent is allowed; a field that is present must be a positive integer naming a model version whose model the writer owns, and is otherwise refused with 403 `resource_not_owned` — a version that is someone else's, missing or deleted, and a value that is not a positive integer, are all refused the same way. The check runs before moderation and before any write budget is spent. Ownership is checked when a document is written, not afterwards: if the model later changes hands or is deleted, documents already stored keep the id.",
476
+ "minItems": 1,
477
+ "maxItems": 4,
478
+ "uniqueItems": true,
479
+ "items": {
480
+ "type": "string",
481
+ "pattern": "^(/[A-Za-z_][A-Za-z0-9_]{0,63}){1,4}$"
482
+ }
471
483
  }
472
484
  },
473
485
  "allOf": [
@@ -484,7 +496,8 @@
484
496
  "counters": false,
485
497
  "marks": false,
486
498
  "showAuthor": false,
487
- "dailyWritesPerAuthor": false
499
+ "dailyWritesPerAuthor": false,
500
+ "ownedModelVersions": false
488
501
  }
489
502
  }
490
503
  },
@@ -504,6 +517,14 @@
504
517
  },
505
518
  "then": { "properties": { "dailyWritesPerAuthor": false } }
506
519
  },
520
+ {
521
+ "$comment": "ownedModelVersions checks the version against the viewer WRITING the document, so it is declared only where that viewer is always the author: shared collections with write:\"own\" (private collections are refused above).",
522
+ "if": {
523
+ "required": ["write"],
524
+ "properties": { "write": { "enum": ["anyone", "owner", "none"] } }
525
+ },
526
+ "then": { "properties": { "ownedModelVersions": false } }
527
+ },
507
528
  {
508
529
  "$comment": "A write:\"none\" collection is written only through counters and marks, so it must declare at least one.",
509
530
  "if": {