@civitai/sdk 0.14.0 → 0.15.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/BREAKING.md CHANGED
@@ -204,8 +204,8 @@ bridge already called, so behaviour is identical on both transports.
204
204
  - **Anonymous viewers may NEVER write or vote.** An anon write is rejected by the write scope's own binding in
205
205
  `withBlockScope`, before the handler runs, so it surfaces as **`403`** rather than the handler's own `401`.
206
206
  - **Authenticated writers must clear a minimum-trust gate.** After the anon check, writes call
207
- `assertSharedWriteTrust` — account age, paid tier, and verified email *or* a linked OAuth account. A signed-in
208
- viewer is not automatically a permitted writer.
207
+ `assertSharedWriteTrust` — account age, completed onboarding, not banned or muted, and verified email *or* a
208
+ linked OAuth account. A signed-in viewer is not automatically a permitted writer.
209
209
  - **Tenancy is structural, not checked.** Every query interpolates a schema name derived from the **verified
210
210
  token**, never from client input. An app cannot address another app's shared storage because it cannot name it.
211
211
  - **Rate limits are per-operation and per-namespace**, so one operation cannot starve another — browsing a feed
package/README.md CHANGED
@@ -96,6 +96,7 @@ the refusal sits at the surface, not at startup:
96
96
  |---|---|
97
97
  | `app.storage.*` | Works. This is the token app storage requires |
98
98
  | `app.sharedStorage.*` | Reads work, anonymous included. Writes additionally need a signed-in viewer past the server's minimum-trust gate, so a `403` there is about the VIEWER, not the token |
99
+ | `app.subListings.*` | Works for a signed-in viewer when the token carries `apps:store:items:write` — never in a dev-token, dev-tunnel or review session |
99
100
  | `app.site` on `blocks/…` | Works. These are the routes the token was minted for |
100
101
  | `app.site.get('me')` and the rest of `/api/v1` | The API's own 401/403, with `auth: "oauth"` named in the message. `status` and `body` are untouched, so a caller can still branch on them |
101
102
  | `app.workflows.*` | Works. Generation for a block goes here, with the `ai:write:budgeted` scope |
@@ -336,7 +337,7 @@ every viewer reads and writes, unlike `app.storage`, which is private per viewer
336
337
  Five methods: `list`, `get`, `append`, `update`, `withdraw`.
337
338
 
338
339
  ```ts
339
- const { items, nextCursor } = await app.sharedStorage.list({ prefix: 'deck:' });
340
+ const { items, nextCursor } = await app.sharedStorage.list({ limit: 20 }); // newest first
340
341
  const { key } = await app.sharedStorage.append({ title: 'My deck', data: { cards } });
341
342
  const row = await app.sharedStorage.get(key); // null when hidden or absent
342
343
  await app.sharedStorage.update(key, { title: 'Renamed' });
@@ -362,9 +363,9 @@ other.** This is the opposite of `app.storage`, so do not carry that gate over:
362
363
  supported path — and may **never** write. An anon write is a `403` from the
363
364
  scope binding, not the `401` a missing token gives.
364
365
  - **A signed-in viewer is not automatically a permitted writer.** Writes clear a
365
- server-side minimum-trust gate (account age, paid tier, verified email or a
366
- linked OAuth account). Treat a write refusal as a normal outcome with a real
367
- message in the UI, not an unexpected error.
366
+ server-side minimum-trust gate (account age, completed onboarding, verified
367
+ email or a linked OAuth account, not banned or muted). Treat a write refusal
368
+ as a normal outcome with a real message in the UI, not an unexpected error.
368
369
 
369
370
  So gate the write affordances on `app.viewer`, and render the reads regardless.
370
371
 
@@ -379,7 +380,9 @@ that type-checks:
379
380
  `app.storage`'s, absence proves the scan completed.
380
381
  - **`append` takes no key.** The server mints a ULID and returns it, so one
381
382
  viewer cannot overwrite another's row. Use `update` for an in-place edit of a
382
- row this viewer authored.
383
+ row this viewer authored. It also means `list`'s `prefix` is no namespace of
384
+ yours: a prefix you choose (`'deck:'`) matches none of the rows `append`
385
+ creates. Put what you filter by in `data` and filter the page yourself.
383
386
  - **`value` is `unknown` on the way out.** The row was written by some other
384
387
  viewer's copy of your app, possibly an older version, so its shape is a fact
385
388
  about the data rather than a promise the client can make. Narrow it yourself.
@@ -426,6 +429,62 @@ an existence oracle, so do not report that as "someone else owns this". Put ever
426
429
  string other viewers will read in `title`/`body`, which the server moderates;
427
430
  `data` is unmoderated app structure.
428
431
 
432
+ ## Store sub-listings
433
+
434
+ `app.subListings` publishes an item a viewer made in your app — a row they
435
+ authored in [shared storage](#shared-storage) — as its **own App Store card**,
436
+ under the viewer's name, badged "in ‹your app›". Three methods over
437
+ `/api/v1/blocks/sub-listings/*`: `upsert`, `withdraw`, `mine`.
438
+
439
+ ```ts
440
+ import { SubListingError } from '@civitai/sdk';
441
+
442
+ try {
443
+ const { id, status, pendingEdit } = await app.subListings.upsert({
444
+ itemKey: 'preset-7f3a', // the key of the viewer's own shared-storage row
445
+ title: 'Moody film portraits',
446
+ subPath: 'p/preset-7f3a', // opens /apps/run/<your-app-slug>/p/preset-7f3a
447
+ tagline: null, // a left-out optional is CLEARED: re-send what you want to keep
448
+ });
449
+ } catch (error) {
450
+ if (error instanceof SubListingError && error.code === 'insufficient_scope') {
451
+ // dev-token, dev-tunnel and review sessions never carry the scope: hide the control
452
+ } else throw error;
453
+ }
454
+
455
+ const { withdrawn } = await app.subListings.withdraw('preset-7f3a');
456
+ const { items } = await app.subListings.mine(); // oldest first, with review status
457
+ ```
458
+
459
+ - **`upsert` replaces the whole item, it does not patch it.** An optional that is
460
+ `null` or `undefined` is left out of the request, and the server stores a
461
+ left-out optional as empty — so an edit that sends only `title` clears the
462
+ item's tagline and image and resets its rating to the app's. Re-send every
463
+ field you want to keep.
464
+ - **The server body is strict** — an unknown key is `400 invalid_body` — so the
465
+ client sends only `itemKey`, `title`, `subPath`, `tagline`, `imageId` and
466
+ `contentRating`, whatever else the object you pass carries. Every other rule (lengths, the `subPath` shape, the rating floor, the image
467
+ checks) is the server's and comes back as a refusal.
468
+ - **A refusal is a `SubListingError`** — an `ApiError` with a typed `code`
469
+ (`not_enabled`, `untrusted`, `item_not_found`, `not_your_item`, `text_rejected`,
470
+ `image_not_public`, `image_not_yours`, `rating_too_loose`, `rate_limited`,
471
+ `author_cap`, `conflict`, `unavailable`, …). Branch on `code`; the message is a
472
+ server sentence, not UI copy. A refusal with no `code` stays a plain `ApiError`.
473
+ A `429`'s `Retry-After` header is not exposed.
474
+ - **Every item waits for a moderator.** `status` is `pending`, `approved`,
475
+ `hidden` or `withdrawn`; an `upsert` answered `hidden` changed nothing, and the
476
+ app cannot lift it. Editing an approved item stages the edit
477
+ (`pendingEdit: true`) while the card keeps serving the approved version.
478
+ - `withdraw` answering `withdrawn: false` is a success: nothing of the viewer's was
479
+ active under that key, or a moderator has hidden it.
480
+
481
+ 🔴 It needs the scope `apps:store:items:write` in the manifest (with a
482
+ `scopeJustifications` entry), and that scope is consent-exempt, refused to an
483
+ anonymous viewer, and **never minted for dev-token, dev-tunnel or review
484
+ sessions** — so these calls only succeed from an approved version of your app,
485
+ and Civitai must also enable your app for sub-listings (`not_enabled` until then).
486
+ Build the publish path to degrade quietly.
487
+
429
488
  ## Multi-step workflows
430
489
 
431
490
  `app.workflows` is how a **block** generates. It goes through the host's
@@ -7,6 +7,7 @@ import { type SharedStorageClient } from '../shared-storage/index.js';
7
7
  import { type DocumentStorage } from '../doc-store/index.js';
8
8
  import { type SiteClient } from '../site/index.js';
9
9
  import { type StorageClient } from '../storage/index.js';
10
+ import { type SubListingsClient } from '../sub-listings/index.js';
10
11
  import { type WorkflowsClient } from '../workflows/index.js';
11
12
  export interface AppClient {
12
13
  /**
@@ -52,6 +53,16 @@ export interface AppClient {
52
53
  * clear a server-side minimum-trust gate.
53
54
  */
54
55
  readonly sharedStorage: SharedStorageClient;
56
+ /**
57
+ * The viewer's own App Store items for this app: `upsert`, `withdraw`, `mine`
58
+ * over the `blocks/sub-listings/*` routes. A refusal the server explained is a
59
+ * `SubListingError` with a typed `code`.
60
+ *
61
+ * 🔴 Requires the block token and `apps:store:items:write`, which is never
62
+ * minted for dev-token, dev-tunnel or review sessions and is refused to an
63
+ * anonymous viewer — so build the publish path to degrade quietly.
64
+ */
65
+ readonly subListings: SubListingsClient;
55
66
  /**
56
67
  * Generation for a BLOCK: estimate, submit, watch and cancel through the
57
68
  * host's `blocks/workflows/*` routes, which price, budget and attribute the
package/dist/app/index.js CHANGED
@@ -8,6 +8,7 @@ import { createSharedStorageClient, } from '../shared-storage/index.js';
8
8
  import { createDocumentStorage } from '../doc-store/index.js';
9
9
  import { createSiteClient, DEFAULT_SITE_URL } from '../site/index.js';
10
10
  import { createStorageClient } from '../storage/index.js';
11
+ import { createSubListingsClient } from '../sub-listings/index.js';
11
12
  import { createWorkflowsClient } from '../workflows/index.js';
12
13
  /** Long enough for a slow host page; short enough that a block opened on its own says so. */
13
14
  const DEFAULT_INIT_TIMEOUT_MS = 10_000;
@@ -82,16 +83,17 @@ const OAUTH_OPT_IN = 'declare `auth: "oauth"` in block.manifest.json';
82
83
  const BLOCK_NAMESPACE = /^\/*blocks\//;
83
84
  function createAppClient(session, options, holdsBlockTokenNow = () => false, holdsOAuthTokenNow = () => false) {
84
85
  const http = (baseUrl) => createHttp({ session, baseUrl, fetch: options.fetch });
85
- // One instance for all four site-hosted surfaces: the storage and workflows
86
- // clients' routes live under the same base URL, so a `siteUrl` override
87
- // redirects them together.
86
+ // One instance for every site-hosted surface: the storage, sub-listings and
87
+ // workflows clients' routes live under the same base URL, so a `siteUrl`
88
+ // override redirects them together.
88
89
  // Wrapped once, so `BLOCK_NAMESPACE` is the single place that decides where the
89
- // diagnosis applies. None of the four `blocks/*` clients needs an exemption
90
+ // diagnosis applies. None of the five `blocks/*` clients needs an exemption
90
91
  // of its own: their routes are `blocks/app-storage/*`, `blocks/shared-storage/*`,
91
- // `blocks/storage/*` and `blocks/workflows/*`, which that namespace already excludes, and
92
- // `test/storage/seam.test.ts`, `test/shared-storage/seam.test.ts`,
93
- // `test/doc-store/client.test.ts` and `test/workflows/seam.test.ts` pin the four
94
- // `BASE` values there textually.
92
+ // `blocks/storage/*`, `blocks/sub-listings/*` and `blocks/workflows/*`, which that
93
+ // namespace already excludes, and `test/storage/seam.test.ts`,
94
+ // `test/shared-storage/seam.test.ts`, `test/doc-store/client.test.ts`,
95
+ // `test/sub-listings/sub-listings.test.ts` and `test/workflows/seam.test.ts` pin the
96
+ // five `BASE` values there textually.
95
97
  const siteHttp = explainApiRefusal(http(options.siteUrl ?? DEFAULT_SITE_URL), holdsBlockTokenNow);
96
98
  return {
97
99
  site: createSiteClient(siteHttp),
@@ -100,6 +102,7 @@ function createAppClient(session, options, holdsBlockTokenNow = () => false, hol
100
102
  // reference to the other.
101
103
  storage: { ...createStorageClient(siteHttp), ...createDocumentStorage(siteHttp) },
102
104
  sharedStorage: createSharedStorageClient(siteHttp),
105
+ subListings: createSubListingsClient(siteHttp),
103
106
  workflows: createWorkflowsClient(explainWorkflowRefusal(siteHttp, holdsOAuthTokenNow)),
104
107
  orchestration: createOrchestrationClient(refuseBlockToken(http(options.orchestrationUrl ?? DEFAULT_ORCHESTRATION_URL), holdsBlockTokenNow)),
105
108
  requestGrants: (scopes, opts) => session.requestGrants(scopes, opts),
package/dist/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export type { GrantOptions, Scope, TokenOptions, TokenSource } from './session/i
11
11
  export type { SiteClient } from './site/index.js';
12
12
  export type { SharedItem, SharedListQuery, SharedListResult, SharedStorageCallOptions, SharedStorageClient, SharedValue, } from './shared-storage/index.js';
13
13
  export type { StorageCallOptions, StorageClient, StorageKeyEntry, StorageListQuery, StorageListResult, StorageQuota, } from './storage/index.js';
14
+ export { SubListingError } from './sub-listings/index.js';
15
+ export type { MySubListing, SubListingCallOptions, SubListingContentRating, SubListingErrorCode, SubListingStatus, SubListingsClient, SubListingUpsertInput, SubListingUpsertResult, SubListingWithdrawResult, } from './sub-listings/index.js';
14
16
  export { StorageError } from './doc-store/index.js';
15
17
  export type { DocumentAuthor, DocumentCallOptions, DocumentListQuery, DocumentPage, DocumentStorage, DocumentWhereValue, StorageCollection, StorageCollectionInfo, StorageDocument, StorageErrorCode, StorageIndexType, StorageInfo, StorageLimits, StorageUsage, WriteOptions, } from './doc-store/index.js';
16
18
  export { SubmissionUnconfirmedError } from './workflows/index.js';
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@
16
16
  export { initialize } from './app/index.js';
17
17
  export { createSignIn, SignInError } from './sign-in/index.js';
18
18
  export { SCOPES } from './session/index.js';
19
+ export { SubListingError } from './sub-listings/index.js';
19
20
  export { StorageError } from './doc-store/index.js';
20
21
  export { SubmissionUnconfirmedError } from './workflows/index.js';
21
22
  export { isTerminal, isTerminalStatus } from './orchestration/index.js';
@@ -51,7 +51,11 @@ export interface SharedItem {
51
51
  viewerVoted: boolean;
52
52
  }
53
53
  export interface SharedListQuery {
54
- /** Narrows to keys starting with this. Escaped server-side against LIKE wildcards. */
54
+ /**
55
+ * Narrows to keys starting with this. Escaped server-side against LIKE wildcards.
56
+ * `append` mints every key as a ULID, so a prefix of your own choosing matches
57
+ * none of the rows it creates.
58
+ */
55
59
  prefix?: string;
56
60
  /** The server bounds this and applies its own default; this client sends none. */
57
61
  limit?: number;
@@ -94,8 +98,9 @@ export interface SharedListResult {
94
98
  * write is refused by the scope binding before the handler runs, so it arrives
95
99
  * as **403**, not the 401 a missing token gives.
96
100
  * - A signed-in viewer is not automatically a permitted writer: writes clear a
97
- * minimum-trust gate (account age, paid tier, verified email or a linked OAuth
98
- * account). Treat a write refusal as a normal outcome and say so in the UI.
101
+ * minimum-trust gate (account age, completed onboarding, verified email or a
102
+ * linked OAuth account, not banned or muted). Treat a write refusal as a
103
+ * normal outcome and say so in the UI.
99
104
  *
100
105
  * 🔴 EVERY FAILURE REJECTS. No path here resolves to mean "not written" or
101
106
  * "could not read". A caller that must not act on a partial view branches on the
@@ -0,0 +1,137 @@
1
+ import { ApiError, type Http } from '../http/index.js';
2
+ export interface SubListingCallOptions {
3
+ signal?: AbortSignal;
4
+ }
5
+ /** A sub-listing's review state. Only `approved` items are in the store. */
6
+ export type SubListingStatus = 'pending' | 'approved' | 'hidden' | 'withdrawn';
7
+ /** The listing rating ladder, least to most mature. */
8
+ export type SubListingContentRating = 'g' | 'pg' | 'pg13' | 'r' | 'x';
9
+ /**
10
+ * What `upsert` publishes: one item the VIEWER made in this app, as its own
11
+ * App Store card under the viewer's name.
12
+ *
13
+ * The server owns every rule (lengths, the `subPath` shape, the rating floor)
14
+ * and answers a breach with a {@link SubListingError}; nothing is pre-checked
15
+ * here, so the rules have one home.
16
+ */
17
+ export interface SubListingUpsertInput {
18
+ /**
19
+ * The key of a row in this app's SHARED storage (`app.sharedStorage`) that the
20
+ * viewer authored and that is not hidden. One sub-listing per key: publishing
21
+ * the same key again edits it, and every call REPLACES the whole item — an
22
+ * optional you leave out is cleared, so re-send the ones you want to keep.
23
+ */
24
+ itemKey: string;
25
+ title: string;
26
+ /** Optional. Left out (`null`/`undefined`), the item has no tagline. */
27
+ tagline?: string | null;
28
+ /**
29
+ * An image the viewer uploaded and that is publicly visible. Left out
30
+ * (`null`/`undefined`), the card shows the app's cover.
31
+ */
32
+ imageId?: number | null;
33
+ /**
34
+ * Where the card opens your app: `/apps/run/<your-app-slug>/<subPath>`. One to
35
+ * four `/`-separated segments of letters, digits, `_` and `-`.
36
+ */
37
+ subPath: string;
38
+ /**
39
+ * No less mature than the app's own rating. Left out (`null`/`undefined`), the
40
+ * item inherits the app's rating.
41
+ */
42
+ contentRating?: SubListingContentRating | null;
43
+ }
44
+ export interface SubListingUpsertResult {
45
+ id: string;
46
+ /**
47
+ * 🔴 `hidden` is an ANSWER, not a failure: a moderator took the item down, the
48
+ * app cannot lift it, and the upsert changed nothing.
49
+ */
50
+ status: SubListingStatus;
51
+ /** True while an edit to an approved item waits for a moderator; the card keeps serving the approved version. */
52
+ pendingEdit: boolean;
53
+ }
54
+ export interface SubListingWithdrawResult {
55
+ ok: true;
56
+ /**
57
+ * 🔴 `false` is a SUCCESS: nothing of this viewer's was active under that key —
58
+ * or a moderator has hidden it, which the app cannot change.
59
+ */
60
+ withdrawn: boolean;
61
+ }
62
+ /** One of the viewer's own items for this app, as `mine` lists it. */
63
+ export interface MySubListing {
64
+ id: string;
65
+ itemKey: string;
66
+ status: SubListingStatus;
67
+ title: string;
68
+ pendingEdit: boolean;
69
+ /** Why a moderator hid it, when one said. */
70
+ statusReason: string | null;
71
+ /** Why a moderator rejected the last staged edit, when one said. */
72
+ editRejectionReason: string | null;
73
+ /** The wire's ISO string, revived once. */
74
+ updatedAt: Date;
75
+ }
76
+ /**
77
+ * The `code` of a sub-listing refusal.
78
+ *
79
+ * The named codes are the ones the three sub-listing routes raise themselves,
80
+ * plus the two token refusals an app should expect here: `insufficient_scope`
81
+ * (the token lacks `apps:store:items:write` — every dev-token, dev-tunnel and
82
+ * review session) and `context_binding` (an anonymous viewer). Any other wire
83
+ * code still arrives, as a plain string.
84
+ */
85
+ export type SubListingErrorCode = 'invalid_body' | 'anonymous' | 'not_enabled' | 'untrusted' | 'item_not_found' | 'not_your_item' | 'text_rejected' | 'image_not_yours' | 'image_not_public' | 'rating_too_loose' | 'rate_limited' | 'author_cap' | 'conflict' | 'unavailable' | 'insufficient_scope' | 'context_binding' | (string & {});
86
+ /**
87
+ * A refusal from a sub-listing route, whose body is `{ error, code }`. Branch on
88
+ * `code`: `error` (this error's `message`) is a server sentence, not UI copy.
89
+ * `status` and `body` are {@link ApiError}'s, as sent.
90
+ *
91
+ * ⚠ A `429` carries a `Retry-After` header this client does not expose
92
+ * (`ApiError` keeps no headers).
93
+ */
94
+ export declare class SubListingError extends ApiError {
95
+ readonly code: SubListingErrorCode;
96
+ constructor(status: number, code: SubListingErrorCode, message: string, body: unknown, options?: ErrorOptions);
97
+ }
98
+ /**
99
+ * The viewer's own App Store items for THIS app — sub-listings of the app's
100
+ * listing, each published by the viewer under their name and reviewed by a
101
+ * moderator before it shows in the store.
102
+ *
103
+ * 🔴 Requires the block token and the scope `apps:store:items:write`, which is
104
+ * consent-exempt, signed-in viewers only, and never minted for dev-token,
105
+ * dev-tunnel or review sessions — so a refusal here is a normal outcome, and the
106
+ * publish path must degrade quietly. The app must also be enabled for
107
+ * sub-listings by Civitai (`not_enabled` until it is).
108
+ *
109
+ * 🔴 EVERY FAILURE REJECTS: a refusal the server explained is a
110
+ * {@link SubListingError}; any other failure is the {@link ApiError} or
111
+ * `CivitaiError` it was.
112
+ */
113
+ export interface SubListingsClient {
114
+ /**
115
+ * Publishes, or edits, the item at `itemKey`. `POST blocks/sub-listings/upsert`.
116
+ *
117
+ * 🔴 The server's body is STRICT — an unknown key is `400 invalid_body` — so
118
+ * only the six fields of {@link SubListingUpsertInput} are sent. An optional
119
+ * that is `null` or `undefined` is left out rather than sent; the server stores
120
+ * either as empty.
121
+ *
122
+ * 🔴 It REPLACES, it does not patch: an edit that leaves out `tagline`,
123
+ * `imageId` or `contentRating` clears it (on an approved item, the cleared
124
+ * version is what goes to the moderator). Re-send every field you want to keep.
125
+ */
126
+ upsert(input: SubListingUpsertInput, opts?: SubListingCallOptions): Promise<SubListingUpsertResult>;
127
+ /** Takes the viewer's item at `itemKey` out of the store. `POST blocks/sub-listings/withdraw`. */
128
+ withdraw(itemKey: string, opts?: SubListingCallOptions): Promise<SubListingWithdrawResult>;
129
+ /**
130
+ * The viewer's items for this app, oldest first, at most 200, with their
131
+ * review state. `GET blocks/sub-listings/mine`.
132
+ */
133
+ mine(opts?: SubListingCallOptions): Promise<{
134
+ items: MySubListing[];
135
+ }>;
136
+ }
137
+ export declare function createSubListingsClient(http: Http): SubListingsClient;
@@ -0,0 +1,122 @@
1
+ import { CivitaiError } from '../core/errors.js';
2
+ import { ApiError } from '../http/index.js';
3
+ /** Where the three routes this client wraps live under the site base URL. */
4
+ const BASE = 'blocks/sub-listings';
5
+ /**
6
+ * A refusal from a sub-listing route, whose body is `{ error, code }`. Branch on
7
+ * `code`: `error` (this error's `message`) is a server sentence, not UI copy.
8
+ * `status` and `body` are {@link ApiError}'s, as sent.
9
+ *
10
+ * ⚠ A `429` carries a `Retry-After` header this client does not expose
11
+ * (`ApiError` keeps no headers).
12
+ */
13
+ export class SubListingError extends ApiError {
14
+ code;
15
+ constructor(status, code, message, body, options) {
16
+ super(status, message, body);
17
+ if (options?.cause !== undefined)
18
+ Object.defineProperty(this, 'cause', { value: options.cause });
19
+ this.name = 'SubListingError';
20
+ this.code = code;
21
+ }
22
+ }
23
+ export function createSubListingsClient(http) {
24
+ const call = async (method, op, body, signal) => {
25
+ try {
26
+ return await http(method, `${BASE}/${op}`, { body, signal });
27
+ }
28
+ catch (error) {
29
+ throw toSubListingError(error);
30
+ }
31
+ };
32
+ return {
33
+ async upsert(input, { signal } = {}) {
34
+ const res = await call('POST', 'upsert', upsertBody(input), signal);
35
+ if (typeof res?.id !== 'string' ||
36
+ typeof res.status !== 'string' ||
37
+ typeof res.pendingEdit !== 'boolean') {
38
+ throw new CivitaiError('sub-listings upsert: malformed reply');
39
+ }
40
+ return {
41
+ id: res.id,
42
+ status: res.status,
43
+ pendingEdit: res.pendingEdit,
44
+ };
45
+ },
46
+ async withdraw(itemKey, { signal } = {}) {
47
+ const res = await call('POST', 'withdraw', { itemKey }, signal);
48
+ if (typeof res?.withdrawn !== 'boolean') {
49
+ throw new CivitaiError('sub-listings withdraw: reply carried no `withdrawn` flag');
50
+ }
51
+ return { ok: true, withdrawn: res.withdrawn };
52
+ },
53
+ async mine({ signal } = {}) {
54
+ const res = await call('GET', 'mine', undefined, signal);
55
+ // 🔴 NO `?? []`. An empty list is a real answer ("nothing in the store"),
56
+ // and a reply without `items` must not be dressed up as it.
57
+ if (!Array.isArray(res?.items)) {
58
+ throw new CivitaiError('sub-listings mine: reply carried no `items` array');
59
+ }
60
+ return { items: res.items.map(toMySubListing) };
61
+ },
62
+ };
63
+ }
64
+ /**
65
+ * The request body: the six fields, named rather than spread, so an extra
66
+ * property on the caller's object never reaches the strict server body; and each
67
+ * optional one only when it holds a value.
68
+ */
69
+ function upsertBody(input) {
70
+ const body = {
71
+ itemKey: input.itemKey,
72
+ title: input.title,
73
+ subPath: input.subPath,
74
+ };
75
+ if (input.tagline != null)
76
+ body.tagline = input.tagline;
77
+ if (input.imageId != null)
78
+ body.imageId = input.imageId;
79
+ if (input.contentRating != null)
80
+ body.contentRating = input.contentRating;
81
+ return body;
82
+ }
83
+ /** An {@link ApiError} whose body carries a string `code` becomes a {@link SubListingError}; anything else passes through. */
84
+ function toSubListingError(error) {
85
+ if (!(error instanceof ApiError) || error instanceof SubListingError)
86
+ return error;
87
+ const body = error.body;
88
+ if (body == null || typeof body !== 'object' || Array.isArray(body))
89
+ return error;
90
+ const { code } = body;
91
+ if (typeof code !== 'string')
92
+ return error;
93
+ return new SubListingError(error.status, code, error.message, body, { cause: error });
94
+ }
95
+ function toMySubListing(row) {
96
+ const r = (row ?? {});
97
+ const nullableString = (v) => v === null || typeof v === 'string';
98
+ if (typeof r.id !== 'string' ||
99
+ typeof r.itemKey !== 'string' ||
100
+ typeof r.status !== 'string' ||
101
+ typeof r.title !== 'string' ||
102
+ typeof r.pendingEdit !== 'boolean' ||
103
+ !nullableString(r.statusReason) ||
104
+ !nullableString(r.editRejectionReason) ||
105
+ typeof r.updatedAt !== 'string') {
106
+ throw new CivitaiError('sub-listings mine: malformed item');
107
+ }
108
+ const updatedAt = new Date(r.updatedAt);
109
+ if (Number.isNaN(updatedAt.getTime())) {
110
+ throw new CivitaiError('sub-listings mine: malformed item timestamp');
111
+ }
112
+ return {
113
+ id: r.id,
114
+ itemKey: r.itemKey,
115
+ status: r.status,
116
+ title: r.title,
117
+ pendingEdit: r.pendingEdit,
118
+ statusReason: r.statusReason,
119
+ editRejectionReason: r.editRejectionReason,
120
+ updatedAt,
121
+ };
122
+ }
@@ -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.14.0";
2
+ export declare const VERSION = "0.15.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.14.0";
3
+ export const VERSION = "0.15.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/sdk",
3
- "version": "0.14.0",
3
+ "version": "0.15.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",