@civitai/sdk 0.14.1 → 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/README.md +57 -0
- package/dist/app/index.d.ts +11 -0
- package/dist/app/index.js +11 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/sub-listings/index.d.ts +137 -0
- package/dist/sub-listings/index.js +122 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +1 -1
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 |
|
|
@@ -428,6 +429,62 @@ an existence oracle, so do not report that as "someone else owns this". Put ever
|
|
|
428
429
|
string other viewers will read in `title`/`body`, which the server moderates;
|
|
429
430
|
`data` is unmoderated app structure.
|
|
430
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
|
+
|
|
431
488
|
## Multi-step workflows
|
|
432
489
|
|
|
433
490
|
`app.workflows` is how a **block** generates. It goes through the host's
|
package/dist/app/index.d.ts
CHANGED
|
@@ -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
|
|
86
|
-
// clients' routes live under the same base URL, so a `siteUrl`
|
|
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
|
|
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
|
|
92
|
-
//
|
|
93
|
-
// `test/
|
|
94
|
-
// `
|
|
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';
|
|
@@ -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.
|
|
2
|
+
export declare const VERSION = "0.15.0";
|
package/package.json
CHANGED