@civitai/sdk 0.15.0 → 0.17.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
@@ -274,7 +274,12 @@ Anonymous viewers may read `shared` collections and nothing else
274
274
  (`sign_in_required`). Creating or editing a document in a viewer-written shared
275
275
  collection needs the consent-gated `apps:storage:publish`; deleting your own, marks
276
276
  and counters need only `apps:storage`. Shared writes pass a minimum-trust gate and
277
- moderation. Only phase-one operations exist: no batch writes, change feed, TTL or
277
+ moderation. A shared `write: "own"` collection whose manifest declares
278
+ `ownedModelVersions` (pointers such as `/workflow/modelVersionId`) refuses a create
279
+ or `put` whose `data` holds, at one of those fields, anything but a model version
280
+ the viewer owns: `resource_not_owned` (403), `details.pointer` naming the field,
281
+ before moderation and before any write budget. An absent field is allowed;
282
+ ownership is checked only when the document is written. Only phase-one operations exist: no batch writes, change feed, TTL or
278
283
  owner-written documents yet.
279
284
 
280
285
  The legacy stores below keep working, unchanged, while apps move; their methods are
@@ -974,6 +979,13 @@ Where it differs from the server on purpose:
974
979
  not a manifest validator otherwise; `civitai app validate` is.
975
980
  - A list `nextCursor` is an opaque token, refused for any other collection, sort or
976
981
  filter than the query that issued it. Treat it as opaque against the server too.
982
+ - It has no model versions of its own, so a collection's `ownedModelVersions` rule
983
+ checks writes against the viewer's `modelVersions` — the ids that viewer owns,
984
+ none by default (so every id is refused, as the server refuses one that is not
985
+ the writer's):
986
+ `fake.as({ viewer: { username: 'bob', modelVersions: [4317] } })`. It does not
987
+ model the server's ownership lookup failing (a retryable 503
988
+ `service_unavailable`).
977
989
 
978
990
  `fake.calls` records every request and answer. Options also cover smaller `limits`,
979
991
  `consent` (revoked scopes, or `'unavailable'`), `appStatus`, `disabled` and a custom
@@ -7,7 +7,7 @@ export interface HostReply {
7
7
  readonly keys: readonly string[] | null;
8
8
  }
9
9
  /** The civitai commit this ledger was read from. A request the host added after it is refused too. */
10
- export declare const HOST_SNAPSHOT = "464b2023e8";
10
+ export declare const HOST_SNAPSHOT = "cd79a91fe4";
11
11
  /**
12
12
  * Every request the host answered at {@link HOST_SNAPSHOT}. A type absent here is refused.
13
13
  * Reply names and framing are fixed at publish time: if the host renames a reply or moves a
@@ -1,7 +1,7 @@
1
1
  // Generated by scripts/gen-host-replies.mjs from snapshots/host-messages.json
2
- // (civitai/civitai 464b2023e8, 2026-10-14). Do not edit.
2
+ // (civitai/civitai cd79a91fe4, 2026-10-17). Do not edit.
3
3
  /** The civitai commit this ledger was read from. A request the host added after it is refused too. */
4
- export const HOST_SNAPSHOT = "464b2023e8";
4
+ export const HOST_SNAPSHOT = "cd79a91fe4";
5
5
  /**
6
6
  * Every request the host answered at {@link HOST_SNAPSHOT}. A type absent here is refused.
7
7
  * Reply names and framing are fixed at publish time: if the host renames a reply or moves a
@@ -22,6 +22,7 @@ export const HOST_REPLIES = {
22
22
  "GET_BUZZ_BALANCE": { "reply": "BUZZ_BALANCE_RESULT", "framing": "legacy", "keys": ["balance"] },
23
23
  "GET_BUZZ_TRANSACTIONS": { "reply": "BUZZ_TRANSACTIONS_RESULT", "framing": "legacy", "keys": ["result"] },
24
24
  "GET_DAILY_COMPENSATION": { "reply": "DAILY_COMPENSATION_RESULT", "framing": "legacy", "keys": ["result"] },
25
+ "GET_ENTITY_REACTIONS": { "reply": "ENTITY_REACTIONS_RESULT", "framing": "legacy", "keys": ["result"] },
25
26
  "GET_IMAGES_BY_IDS": { "reply": "IMAGES_RESULT", "framing": "legacy", "keys": ["result"] },
26
27
  "GET_VIEWER": { "reply": "VIEWER_RESULT", "framing": "legacy", "keys": ["viewer"] },
27
28
  "GET_WILDCARD_PACK": { "reply": "WILDCARD_PACK_RESULT", "framing": "legacy", "keys": ["pack"] },
@@ -29,6 +30,7 @@ export const HOST_REPLIES = {
29
30
  "OPEN_CHECKPOINT_PICKER": { "reply": "CHECKPOINT_PICKER_RESULT", "framing": "legacy", "keys": ["selected"] },
30
31
  "OPEN_IMAGE_PICKER": { "reply": "IMAGE_PICKER_RESULT", "framing": "legacy", "keys": ["selected", "selectedImages"] },
31
32
  "OPEN_IMAGE_UPLOAD": { "reply": "IMAGE_UPLOAD_RESULT", "framing": "legacy", "keys": ["selected"] },
33
+ "OPEN_MEDIA_UPLOAD": { "reply": "MEDIA_UPLOAD_RESULT", "framing": "legacy", "keys": ["selected"] },
32
34
  "OPEN_RESOURCE_PICKER": { "reply": "RESOURCE_PICKER_RESULT", "framing": "legacy", "keys": ["selected", "selectedResources"] },
33
35
  "POLL_WORKFLOW": { "reply": "WORKFLOW_STATUS", "framing": "legacy", "keys": ["snapshot"] },
34
36
  "PREPARE_TRAINING_DATASET": { "reply": "TRAINING_DATASET_RESULT", "framing": "legacy", "keys": ["result"] },
@@ -39,6 +41,7 @@ export const HOST_REPLIES = {
39
41
  "RUN_TRAINING": { "reply": "TRAINING_RESULT", "framing": "legacy", "keys": ["snapshot"] },
40
42
  "SAVE_IMAGE": { "reply": "SAVE_IMAGE_RESULT", "framing": "legacy", "keys": ["ok"] },
41
43
  "SET_COLLECTION_FOLLOW": { "reply": "COLLECTION_FOLLOW_RESULT", "framing": "legacy", "keys": ["result"] },
44
+ "SET_ENTITY_REACTION": { "reply": "ENTITY_REACTION_RESULT", "framing": "legacy", "keys": ["result"] },
42
45
  "SET_USER_CHECKPOINT": { "reply": "USER_CHECKPOINT_SET", "framing": "legacy", "keys": ["ok"] },
43
46
  "SHARED_APPEND": { "reply": "SHARED_APPEND_RESULT", "framing": "legacy", "keys": ["key"] },
44
47
  "SHARED_GET": { "reply": "SHARED_GET_RESULT", "framing": "legacy", "keys": ["item"] },
@@ -25,6 +25,13 @@ export interface FakeStorageViewer {
25
25
  trusted?: boolean;
26
26
  /** `details.reason` when untrusted. Default `account_too_new`. */
27
27
  untrustedReason?: string;
28
+ /**
29
+ * Ids of the model versions whose model this viewer owns — what a collection's
30
+ * `ownedModelVersions` rule checks a write against. Default: none, so every id
31
+ * at a declared field is refused `resource_not_owned`, as the server refuses a
32
+ * version that is not the writer's (or does not exist).
33
+ */
34
+ modelVersions?: readonly number[];
28
35
  }
29
36
  /** Who is calling: a viewer (or `null`, anonymous) holding a token of some kind and scopes. */
30
37
  export interface FakeStorageCaller {
@@ -18,10 +18,15 @@ import { createDocumentStorage } from './index.js';
18
18
  * the fake is CREATED rather than per request; and a list cursor is an opaque token
19
19
  * bound to its whole query, not the server's sort-bound keyset string.
20
20
  *
21
+ * A collection's `ownedModelVersions` rule is enforced; the fake has no model
22
+ * versions of its own, so which ones a viewer owns is `FakeStorageViewer.modelVersions`
23
+ * (default none).
24
+ *
21
25
  * NOT MODELLED: moderator hides (a hidden document reads as not found on the server;
22
26
  * nothing here hides one), moderator exemption from the scanner, the review-preview
23
- * namespace, and the token middleware's other refusals (revoked instance, context
24
- * binding, an approval read that fails).
27
+ * namespace, the token middleware's other refusals (revoked instance, context
28
+ * binding, an approval read that fails), and a failed ownership lookup (the
29
+ * server's retryable 503 `service_unavailable`).
25
30
  */
26
31
  // ---------------------------------------------------------------------------
27
32
  // Bounds — the server's, one copy here (the contract run pins the ones it reaches)
@@ -85,6 +90,7 @@ const STATUS = {
85
90
  not_eligible: 403,
86
91
  not_author: 403,
87
92
  write_policy: 403,
93
+ resource_not_owned: 403,
88
94
  not_found: 404,
89
95
  collection_not_declared: 404,
90
96
  method_not_allowed: 405,
@@ -97,6 +103,11 @@ const STATUS = {
97
103
  moderation_unavailable: 503,
98
104
  consent_unavailable: 503,
99
105
  };
106
+ /** The manifest's `ownedModelVersions` pointer: 1 to 4 plain identifier segments. */
107
+ const OWNED_POINTER = /^(\/[A-Za-z_][A-Za-z0-9_]{0,63}){1,4}$/u;
108
+ const MAX_OWNED_POINTERS = 4;
109
+ /** Postgres `integer`: the server refuses a larger id before querying for it. */
110
+ const INT4_MAX = 2_147_483_647;
100
111
  const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
101
112
  /** Declarations the platform does not serve yet, each with what to do instead. */
102
113
  const PHASED = [
@@ -144,12 +155,30 @@ function parseManifest(manifest) {
144
155
  else
145
156
  errors.push(`${at}.indexes.${path}: type must be one of ${INDEX_TYPES.join(', ')}`);
146
157
  }
158
+ const ownedModelVersions = [];
159
+ if (raw.ownedModelVersions !== undefined) {
160
+ const list = raw.ownedModelVersions;
161
+ if (!shared || raw.write !== 'own') {
162
+ errors.push(`${at}.ownedModelVersions: applies only to shared collections with write "own"`);
163
+ }
164
+ else if (!Array.isArray(list) ||
165
+ list.length < 1 ||
166
+ list.length > MAX_OWNED_POINTERS ||
167
+ !list.every((p) => typeof p === 'string' && OWNED_POINTER.test(p)) ||
168
+ new Set(list).size !== list.length) {
169
+ errors.push(`${at}.ownedModelVersions: must be 1 to ${MAX_OWNED_POINTERS} distinct pointers such as "/workflow/modelVersionId"`);
170
+ }
171
+ else {
172
+ ownedModelVersions.push(...list);
173
+ }
174
+ }
147
175
  const decl = {
148
176
  visibility: raw.visibility,
149
177
  counters: shared ? names(raw.counters) : [],
150
178
  marks: shared ? names(raw.marks) : [],
151
179
  indexes,
152
180
  showAuthor: shared && raw.showAuthor === true,
181
+ ownedModelVersions,
153
182
  };
154
183
  if (shared && (raw.write === 'own' || raw.write === 'none'))
155
184
  decl.write = raw.write;
@@ -893,6 +922,41 @@ export function createFakeStorage(options) {
893
922
  });
894
923
  }
895
924
  }
925
+ /**
926
+ * The collection's `ownedModelVersions` rule, as the server's
927
+ * `doc-store-ownership.ts` applies it: a pointer that resolves to nothing is
928
+ * allowed; every value found must be a positive `integer` id — all of them are
929
+ * checked before any ownership — and then the writer's own version. One code
930
+ * and message for every reason, so a refusal reveals nothing about whether a
931
+ * version exists.
932
+ */
933
+ function assertOwnedModelVersions(decl, data, viewer) {
934
+ if (decl.ownedModelVersions.length === 0)
935
+ return;
936
+ const notOwned = (pointer) => refuse('resource_not_owned', 'the model version at this field is not one the author owns', { pointer });
937
+ const claimed = [];
938
+ for (const pointer of decl.ownedModelVersions) {
939
+ let cur = data;
940
+ let found = true;
941
+ for (const seg of pointer.split('/').slice(1)) {
942
+ if (!isObject(cur) || !Object.prototype.hasOwnProperty.call(cur, seg)) {
943
+ found = false;
944
+ break;
945
+ }
946
+ cur = cur[seg];
947
+ }
948
+ if (!found)
949
+ continue;
950
+ if (typeof cur !== 'number' || !Number.isInteger(cur) || cur < 1 || cur > INT4_MAX)
951
+ throw notOwned(pointer);
952
+ claimed.push({ pointer, id: cur });
953
+ }
954
+ const owned = new Set(viewer.modelVersions ?? []);
955
+ for (const { pointer, id } of claimed) {
956
+ if (!owned.has(id))
957
+ throw notOwned(pointer);
958
+ }
959
+ }
896
960
  /** The server's refusals that are ITS fault: refunded without a rejection charge. */
897
961
  const isOurFault = (err) => !(err instanceof Refusal) ||
898
962
  (err.code === 'rate_limited' ? err.details?.reason === 'contention' : ['storage_disabled', 'moderation_unavailable', 'consent_unavailable'].includes(err.code));
@@ -1032,6 +1096,11 @@ export function createFakeStorage(options) {
1032
1096
  throw invalid('ifVersion must be a non-negative integer');
1033
1097
  deleteVersion = Number(raw);
1034
1098
  }
1099
+ // Every write that sets `data`, before the trust gate, the write budget and the
1100
+ // moderation scan (and so before the store's own checks), as the server orders it.
1101
+ const newData = created?.data ?? putIn?.data;
1102
+ if (newData)
1103
+ assertOwnedModelVersions(decl, newData, viewer);
1035
1104
  const write = (moderated, landed) => {
1036
1105
  if (created) {
1037
1106
  if (moderated)
@@ -171,7 +171,7 @@ export interface DocumentStorage {
171
171
  * Every code the server documents for these routes. The type stays open
172
172
  * (`string & {}`) so a code a later server adds still reaches the caller.
173
173
  */
174
- export type StorageErrorCode = 'invalid_request' | 'index_not_declared' | 'unauthenticated' | 'sign_in_required' | 'insufficient_scope' | 'consent_required' | 'not_eligible' | 'not_author' | 'write_policy' | 'not_found' | 'collection_not_declared' | 'method_not_allowed' | 'version_conflict' | 'index_building' | 'cursor_expired' | 'too_large' | 'quota_exceeded' | 'content_rejected' | 'rate_limited' | 'internal_error' | 'storage_disabled' | 'moderation_unavailable' | 'consent_unavailable' | 'service_unavailable' | (string & {});
174
+ export type StorageErrorCode = 'invalid_request' | 'index_not_declared' | 'unauthenticated' | 'sign_in_required' | 'insufficient_scope' | 'consent_required' | 'not_eligible' | 'not_author' | 'write_policy' | 'resource_not_owned' | 'not_found' | 'collection_not_declared' | 'method_not_allowed' | 'version_conflict' | 'index_building' | 'cursor_expired' | 'too_large' | 'quota_exceeded' | 'content_rejected' | 'rate_limited' | 'internal_error' | 'storage_disabled' | 'moderation_unavailable' | 'consent_unavailable' | 'service_unavailable' | (string & {});
175
175
  /**
176
176
  * A refusal from the document store, in its one envelope `{ code, message, error, details? }`.
177
177
  * Branch on `code`; `status` and `body` are {@link ApiError}'s, as sent.
@@ -1,5 +1,5 @@
1
1
  /** Every scope an app can hold, as the site names them. */
2
- export declare const SCOPES: readonly ['ai:write:budgeted', 'apps:storage', 'apps:storage:publish', 'apps:storage:read', 'apps:storage:shared:read', 'apps:storage:shared:write', 'apps:storage:write', 'apps:store:items:write', 'buzz:read:self', 'collections:read:private', 'collections:read:self', 'collections:write:self', 'goods:purchase:self', 'goods:read:self', 'models:read:self', 'posts:write:self', 'social:tip:self', 'user:read:self'];
2
+ export declare const SCOPES: readonly ['ai:write:budgeted', 'apps:storage', 'apps:storage:publish', 'apps:storage:read', 'apps:storage:shared:read', 'apps:storage:shared:write', 'apps:storage:write', 'apps:store:items:write', 'buzz:read:self', 'collections:read:private', 'collections:read:self', 'collections:write:self', 'goods:purchase:self', 'goods:read:self', 'models:read:self', 'posts:write:self', 'social:react:self', 'social:tip:self', 'user:read:self'];
3
3
  export type Scope = (typeof SCOPES)[number];
4
4
  export interface TokenOptions {
5
5
  /** Skip the held token and get a new one, e.g. after the API refused it. */
@@ -16,6 +16,7 @@ export const SCOPES = [
16
16
  'goods:read:self',
17
17
  'models:read:self',
18
18
  'posts:write:self',
19
+ 'social:react:self',
19
20
  'social:tip:self',
20
21
  'user:read:self',
21
22
  ];
@@ -1,2 +1,2 @@
1
1
  /** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
2
- export declare const VERSION = "0.15.0";
2
+ export declare const VERSION = "0.17.0";
@@ -1,3 +1,3 @@
1
1
  /* AUTOGENERATED from package.json by scripts/gen-version.mjs. DO NOT EDIT. */
2
2
  /** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
3
- export const VERSION = "0.15.0";
3
+ export const VERSION = "0.17.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/sdk",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "The Civitai SDK: an app holds a token and calls the Civitai API and the orchestrator; a block asks its host only to show host UI.",
5
5
  "license": "MIT",
6
6
  "type": "module",