@civitai/sdk 0.13.1 → 0.14.1

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.
@@ -0,0 +1,187 @@
1
+ import { ApiError, type Http } from '../http/index.js';
2
+ export interface DocumentCallOptions {
3
+ signal?: AbortSignal;
4
+ }
5
+ /** Who wrote a document, as every reader sees it. Never a numeric user id. */
6
+ export type DocumentAuthor =
7
+ /**
8
+ * A viewer. `isViewer` is whether that is the viewer reading it; `username` is
9
+ * present only when the collection declares `showAuthor` (and the user has one).
10
+ */
11
+ {
12
+ kind: 'user';
13
+ isViewer: boolean;
14
+ username?: string;
15
+ }
16
+ /** Written by the app owner (a later phase; nothing writes one yet). */
17
+ | {
18
+ kind: 'app';
19
+ }
20
+ /** Created by the first increment of a counter in a `write: "none"` collection; nobody owns it. */
21
+ | {
22
+ kind: 'counter';
23
+ };
24
+ export interface StorageDocument<T extends object = Record<string, unknown>> {
25
+ key: string;
26
+ data: T;
27
+ /** +1 on every `data` write. Counter and mark changes do not move it. */
28
+ version: number;
29
+ /** Revived from the wire's ISO string, once, here. */
30
+ createdAt: Date;
31
+ updatedAt: Date;
32
+ author: DocumentAuthor;
33
+ /** Declared counters of a shared collection; `{}` on a private one. */
34
+ counters: Record<string, number>;
35
+ /** Declared mark counts of a shared collection (never `report`'s); `{}` on a private one. */
36
+ marks: Record<string, number>;
37
+ /** The viewer's own marks on this document; `null` for an anonymous viewer. */
38
+ viewer: {
39
+ marks: string[];
40
+ } | null;
41
+ }
42
+ /** A `time` index filters by an ISO-8601 string or epoch milliseconds; a `Date` is sent as ISO. */
43
+ export type DocumentWhereValue = string | number | boolean | Date;
44
+ export interface DocumentListQuery {
45
+ /**
46
+ * Equality on ONE declared index: `{ status: 'open' }`. An undeclared field is
47
+ * refused `index_not_declared`, more than one field `invalid_request`.
48
+ */
49
+ where?: Record<string, DocumentWhereValue>;
50
+ /**
51
+ * `[-]key | createdAt | updatedAt | <index> | counter:<name> | mark:<name>`; a
52
+ * leading `-` is descending. The server's default is `key`.
53
+ */
54
+ sort?: string;
55
+ /** Only documents the viewer authored. */
56
+ author?: 'me';
57
+ /** With {@link DocumentListQuery.mark}: only documents the viewer marked with it. */
58
+ markedBy?: 'me';
59
+ mark?: string;
60
+ keyPrefix?: string;
61
+ /** The server bounds it and applies its own default; this client sends none. */
62
+ limit?: number;
63
+ /** A `nextCursor` from the previous page of the SAME query. */
64
+ cursor?: string;
65
+ }
66
+ export interface DocumentPage<T extends object = Record<string, unknown>> {
67
+ items: StorageDocument<T>[];
68
+ /** `null` exactly when this was the last page. Passed through as the server sent it. */
69
+ nextCursor: string | null;
70
+ }
71
+ export interface WriteOptions extends DocumentCallOptions {
72
+ /**
73
+ * Write only if the document is at this version — `0` means only if it does not
74
+ * exist yet. A mismatch rejects with `version_conflict` and
75
+ * `details.currentVersion`.
76
+ */
77
+ ifVersion?: number;
78
+ }
79
+ export interface StorageCollection<T extends object = Record<string, unknown>> {
80
+ /** The document, or `null` when there is none (missing, hidden and absent read alike). */
81
+ get(key: string, opts?: DocumentCallOptions): Promise<StorageDocument<T> | null>;
82
+ /** One round trip; the result is in the order of `keys`, `null` for each miss. */
83
+ getMany(keys: readonly string[], opts?: DocumentCallOptions): Promise<(StorageDocument<T> | null)[]>;
84
+ /** Create or replace the document at `key`. */
85
+ put(key: string, data: T, opts?: WriteOptions): Promise<StorageDocument<T>>;
86
+ /** Create a document under a key the server chooses (a sortable id). */
87
+ create(data: T, opts?: DocumentCallOptions): Promise<StorageDocument<T>>;
88
+ /** Idempotent: `deleted: false` means there was nothing to delete. */
89
+ delete(key: string, opts?: WriteOptions): Promise<{
90
+ deleted: boolean;
91
+ }>;
92
+ /** One page. Follow `nextCursor` with the same query for the next one. */
93
+ list(query?: DocumentListQuery, opts?: DocumentCallOptions): Promise<DocumentPage<T>>;
94
+ /**
95
+ * Add `by` (the server's default when omitted) to a declared counter of a
96
+ * shared document. In a `write: "none"` collection the first increment of an
97
+ * absent key creates the document.
98
+ */
99
+ increment(key: string, counter: string, by?: number, opts?: DocumentCallOptions): Promise<{
100
+ value: number;
101
+ }>;
102
+ /**
103
+ * Set the viewer's mark on a shared document — a declared mark, or the
104
+ * built-in `report`. Idempotent. `count` is absent for `report`, whose count is
105
+ * never exposed.
106
+ */
107
+ mark(key: string, name: string, opts?: DocumentCallOptions): Promise<{
108
+ marked: true;
109
+ count?: number;
110
+ }>;
111
+ /**
112
+ * Remove the viewer's mark. Idempotent. A `report` cannot be withdrawn: the
113
+ * server refuses it `invalid_request` (400) for every caller.
114
+ */
115
+ unmark(key: string, name: string, opts?: DocumentCallOptions): Promise<{
116
+ marked: false;
117
+ count?: number;
118
+ }>;
119
+ }
120
+ export type StorageIndexType = 'string' | 'number' | 'boolean' | 'time';
121
+ /** One declared collection, as the server reads the app's approved manifest. */
122
+ export interface StorageCollectionInfo {
123
+ kind: string;
124
+ visibility: 'private' | 'shared';
125
+ /** Shared collections only. */
126
+ write?: string;
127
+ counters: string[];
128
+ marks: string[];
129
+ indexes: Record<string, StorageIndexType>;
130
+ showAuthor: boolean;
131
+ }
132
+ export interface StorageUsage {
133
+ /** The STORED unit (the database's own measure of the JSON), which the ceilings bind in. */
134
+ bytes: number;
135
+ docs: number;
136
+ }
137
+ /** Render these, never a compiled-in figure: they are the server's and can move. */
138
+ export interface StorageLimits {
139
+ docBytes: number;
140
+ appBytes: number;
141
+ appDocs: number;
142
+ viewerBytes: number;
143
+ viewerDocs: number;
144
+ listLimitMax: number;
145
+ getManyMax: number;
146
+ rate: {
147
+ readsPerMinute: number;
148
+ privateWritesPerMinute: number;
149
+ marksAndCountersPerMinute: number;
150
+ reportsPerDay: number;
151
+ sharedWritesBurst: number;
152
+ sharedWritesPerDay: number;
153
+ };
154
+ }
155
+ export interface StorageInfo {
156
+ collections: Record<string, StorageCollectionInfo>;
157
+ limits: StorageLimits;
158
+ usage: {
159
+ app: StorageUsage;
160
+ viewer: StorageUsage;
161
+ };
162
+ }
163
+ /** The document store's surface on `app.storage`. */
164
+ export interface DocumentStorage {
165
+ /** A declared collection. Nothing is sent until an operation is called. */
166
+ collection<T extends object = Record<string, unknown>>(name: string): StorageCollection<T>;
167
+ /** The declared collections, the limits and the usage so far. Signed-in viewers only. */
168
+ info(opts?: DocumentCallOptions): Promise<StorageInfo>;
169
+ }
170
+ /**
171
+ * Every code the server documents for these routes. The type stays open
172
+ * (`string & {}`) so a code a later server adds still reaches the caller.
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 & {});
175
+ /**
176
+ * A refusal from the document store, in its one envelope `{ code, message, error, details? }`.
177
+ * Branch on `code`; `status` and `body` are {@link ApiError}'s, as sent.
178
+ */
179
+ export declare class StorageError extends ApiError {
180
+ readonly code: StorageErrorCode;
181
+ /** E.g. `currentVersion`, `reason`, `category`, `ceiling`, `bucket`, `retryAfter`. */
182
+ readonly details?: Record<string, unknown>;
183
+ constructor(status: number, code: StorageErrorCode, message: string, body: unknown, details?: Record<string, unknown>, options?: ErrorOptions);
184
+ /** Seconds to wait before retrying, when the server named one (`rate_limited`, a retryable 503). */
185
+ get retryAfter(): number | undefined;
186
+ }
187
+ export declare function createDocumentStorage(http: Http): DocumentStorage;
@@ -0,0 +1,260 @@
1
+ import { CivitaiError } from '../core/errors.js';
2
+ import { ApiError } from '../http/index.js';
3
+ /**
4
+ * The App Blocks DOCUMENT STORE: named collections of JSON documents, declared in
5
+ * the app's manifest under `storage.collections`, served by `/api/v1/blocks/storage/…`.
6
+ *
7
+ * It replaces the two key/value stores (`app.storage.get/set/…` and
8
+ * `app.sharedStorage`), which keep working, unchanged, until every app has moved.
9
+ *
10
+ * The rules — who may read or write which collection, the limits, the error codes
11
+ * — are the server's, and this client re-spells none of them. It sends a request,
12
+ * reads the reply, and turns a refusal into a {@link StorageError} carrying the
13
+ * server's own `code`. It refuses only what would reach a DIFFERENT route: a path
14
+ * segment a URL cannot carry (`''`, `.`, `..`), which `fetch` would rewrite, and a
15
+ * collection name outside the manifest's rule (`_get` is itself a route) — each with
16
+ * the code the server gives the same input.
17
+ */
18
+ /** Where the routes live under the site base URL (`…/api/v1`). */
19
+ const BASE = 'blocks/storage';
20
+ /** The batch-get route sits at the collection position behind a `_`, which no collection name can start with. */
21
+ const GET_MANY = '_get';
22
+ /**
23
+ * A refusal from the document store, in its one envelope `{ code, message, error, details? }`.
24
+ * Branch on `code`; `status` and `body` are {@link ApiError}'s, as sent.
25
+ */
26
+ export class StorageError extends ApiError {
27
+ code;
28
+ /** E.g. `currentVersion`, `reason`, `category`, `ceiling`, `bucket`, `retryAfter`. */
29
+ details;
30
+ constructor(status, code, message, body, details, options) {
31
+ super(status, message, body);
32
+ if (options?.cause !== undefined)
33
+ Object.defineProperty(this, 'cause', { value: options.cause });
34
+ this.name = 'StorageError';
35
+ this.code = code;
36
+ if (details !== undefined)
37
+ this.details = details;
38
+ }
39
+ /** Seconds to wait before retrying, when the server named one (`rate_limited`, a retryable 503). */
40
+ get retryAfter() {
41
+ const value = this.details?.retryAfter;
42
+ return typeof value === 'number' ? value : undefined;
43
+ }
44
+ }
45
+ /** An {@link ApiError} whose body is the store's envelope becomes a {@link StorageError}; anything else passes through. */
46
+ function toStorageError(error) {
47
+ if (!(error instanceof ApiError) || error instanceof StorageError)
48
+ return error;
49
+ const body = error.body;
50
+ if (body == null || typeof body !== 'object' || Array.isArray(body))
51
+ return error;
52
+ const { code, message, details } = body;
53
+ if (typeof code !== 'string')
54
+ return error;
55
+ return new StorageError(error.status, code, typeof message === 'string' ? message : error.message, body, details != null && typeof details === 'object' && !Array.isArray(details)
56
+ ? details
57
+ : undefined, { cause: error });
58
+ }
59
+ /**
60
+ * The manifest's rule for a collection name. A name outside it can never be
61
+ * declared, and some of those (`_get`, `_drafts`) are ROUTES at that position, which
62
+ * the server answers with a route-level `not_found` — which `get` would read as "no
63
+ * such document". So such a name is refused here, before sending, with the code the
64
+ * server gives any undeclared collection.
65
+ */
66
+ const COLLECTION_NAME = /^[a-z][a-z0-9_]{0,31}$/;
67
+ function collectionSegment(name) {
68
+ if (typeof name !== 'string' || !COLLECTION_NAME.test(name)) {
69
+ const status = 404;
70
+ const message = `collection ${JSON.stringify(name)} is not a valid collection name (refused before sending)`;
71
+ const details = { collection: name };
72
+ throw new StorageError(status, 'collection_not_declared', message, { code: 'collection_not_declared', message, error: message, details }, details);
73
+ }
74
+ return name;
75
+ }
76
+ /**
77
+ * A path segment from caller input. Encoded, so `/`, `?`, `#` and spaces stay
78
+ * inside it; and refused when URL resolution would rewrite it — an empty segment
79
+ * collapses and `.`/`..` are path steps, so each would address a different route.
80
+ * Refused with the code the server gives the same key, so a caller branches once.
81
+ */
82
+ function segment(what, value) {
83
+ if (typeof value !== 'string' || value === '' || value === '.' || value === '..') {
84
+ const status = 400;
85
+ const message = `${what} ${JSON.stringify(value)} cannot be addressed in a URL path (refused before sending)`;
86
+ throw new StorageError(status, 'invalid_request', message, { code: 'invalid_request', message, error: message });
87
+ }
88
+ return encodeURIComponent(value);
89
+ }
90
+ export function createDocumentStorage(http) {
91
+ const call = async (method, path, opts = {}) => {
92
+ try {
93
+ return await http(method, path, opts);
94
+ }
95
+ catch (error) {
96
+ throw toStorageError(error);
97
+ }
98
+ };
99
+ return {
100
+ collection(name) {
101
+ // Resolved per call, inside each async operation, so a bad name rejects that
102
+ // call rather than throwing from `collection()` itself.
103
+ const coll = () => collectionSegment(name);
104
+ const docPath = (key) => `${BASE}/${coll()}/${segment('key', key)}`;
105
+ const doc = (reply, op) => toDocument(reply, `${name}.${op}`);
106
+ return {
107
+ async get(key, { signal } = {}) {
108
+ try {
109
+ return doc(await call('GET', docPath(key), { signal }), 'get');
110
+ }
111
+ catch (error) {
112
+ // Only THIS code is "no such document". `collection_not_declared` is a 404
113
+ // too, and is a bug in the caller, never an empty answer.
114
+ if (error instanceof StorageError && error.code === 'not_found')
115
+ return null;
116
+ throw error;
117
+ }
118
+ },
119
+ async getMany(keys, { signal } = {}) {
120
+ const reply = await call('POST', `${BASE}/${GET_MANY}/${coll()}`, {
121
+ body: { keys: [...keys] },
122
+ signal,
123
+ });
124
+ if (!Array.isArray(reply?.items) || reply.items.length !== keys.length) {
125
+ throw new CivitaiError(`storage ${name}.getMany: reply carried no \`items\` array of ${keys.length}`);
126
+ }
127
+ return reply.items.map((item) => (item === null ? null : doc(item, 'getMany')));
128
+ },
129
+ async put(key, data, { ifVersion, signal } = {}) {
130
+ // Named rather than spread: `JSON.stringify` drops an undefined `ifVersion`,
131
+ // so an unconditional put sends `{ data }` and nothing else.
132
+ return doc(await call('PUT', docPath(key), { body: { data, ifVersion }, signal }), 'put');
133
+ },
134
+ async create(data, { signal } = {}) {
135
+ return doc(await call('POST', `${BASE}/${coll()}`, { body: { data }, signal }), 'create');
136
+ },
137
+ async delete(key, { ifVersion, signal } = {}) {
138
+ const reply = await call('DELETE', docPath(key), {
139
+ query: { ifVersion },
140
+ signal,
141
+ });
142
+ if (typeof reply?.deleted !== 'boolean') {
143
+ throw new CivitaiError(`storage ${name}.delete: reply carried no \`deleted\` flag`);
144
+ }
145
+ return { deleted: reply.deleted };
146
+ },
147
+ async list(query = {}, { signal } = {}) {
148
+ const params = {
149
+ sort: query.sort,
150
+ author: query.author,
151
+ markedBy: query.markedBy,
152
+ mark: query.mark,
153
+ keyPrefix: query.keyPrefix,
154
+ limit: query.limit,
155
+ cursor: query.cursor,
156
+ };
157
+ for (const [field, value] of Object.entries(query.where ?? {})) {
158
+ params[`where[${field}]`] = value instanceof Date ? value.toISOString() : value;
159
+ }
160
+ const reply = await call('GET', `${BASE}/${coll()}`, {
161
+ query: params,
162
+ signal,
163
+ });
164
+ // 🔴 No `?? []` and no `?? null`: a malformed 200 must throw, never read as
165
+ // "nothing here" or "last page".
166
+ if (!Array.isArray(reply?.items)) {
167
+ throw new CivitaiError(`storage ${name}.list: reply carried no \`items\` array`);
168
+ }
169
+ if (reply.nextCursor !== null && typeof reply.nextCursor !== 'string') {
170
+ throw new CivitaiError(`storage ${name}.list: reply carried no \`nextCursor\``);
171
+ }
172
+ return { items: reply.items.map((item) => doc(item, 'list')), nextCursor: reply.nextCursor };
173
+ },
174
+ async increment(key, counter, by, { signal } = {}) {
175
+ const reply = await call('POST', `${docPath(key)}/counters/${segment('counter', counter)}`,
176
+ // No body at all when `by` is omitted: the server's default applies.
177
+ { body: by === undefined ? undefined : { by }, signal });
178
+ if (typeof reply?.value !== 'number') {
179
+ throw new CivitaiError(`storage ${name}.increment: reply carried no \`value\``);
180
+ }
181
+ return { value: reply.value };
182
+ },
183
+ async mark(key, markName, { signal } = {}) {
184
+ const reply = await call('PUT', `${docPath(key)}/marks/${segment('mark', markName)}`, { signal });
185
+ return toMarkResult(reply, true, `${name}.mark`);
186
+ },
187
+ async unmark(key, markName, { signal } = {}) {
188
+ const reply = await call('DELETE', `${docPath(key)}/marks/${segment('mark', markName)}`, { signal });
189
+ return toMarkResult(reply, false, `${name}.unmark`);
190
+ },
191
+ };
192
+ },
193
+ async info({ signal } = {}) {
194
+ const reply = await call('GET', BASE, { signal });
195
+ if (reply == null ||
196
+ typeof reply !== 'object' ||
197
+ typeof reply.collections !== 'object' ||
198
+ typeof reply.limits !== 'object' ||
199
+ typeof reply.usage !== 'object') {
200
+ throw new CivitaiError('storage info: reply carried no `collections`, `limits` and `usage`');
201
+ }
202
+ return reply;
203
+ },
204
+ };
205
+ }
206
+ function toMarkResult(reply, marked, op) {
207
+ const r = (reply ?? {});
208
+ if (r.marked !== marked || (r.count !== undefined && typeof r.count !== 'number')) {
209
+ throw new CivitaiError(`storage ${op}: malformed reply`);
210
+ }
211
+ return r.count === undefined ? { marked } : { marked, count: r.count };
212
+ }
213
+ const isRecord = (value) => value != null && typeof value === 'object' && !Array.isArray(value);
214
+ function toDate(value, op, field) {
215
+ // `new Date(null)` is the epoch, not an Invalid Date: the type check is the gate.
216
+ const at = typeof value === 'string' ? new Date(value) : new Date(Number.NaN);
217
+ if (Number.isNaN(at.getTime()))
218
+ throw new CivitaiError(`storage ${op}: malformed \`${field}\``);
219
+ return at;
220
+ }
221
+ function toAuthor(value, op) {
222
+ if (isRecord(value)) {
223
+ // Every field copied by name, so nothing else the wire carries reaches a reader.
224
+ if (value.kind === 'app' || value.kind === 'counter')
225
+ return { kind: value.kind };
226
+ if (value.kind === 'user' && typeof value.isViewer === 'boolean') {
227
+ return typeof value.username === 'string'
228
+ ? { kind: 'user', isViewer: value.isViewer, username: value.username }
229
+ : { kind: 'user', isViewer: value.isViewer };
230
+ }
231
+ }
232
+ throw new CivitaiError(`storage ${op}: malformed \`author\``);
233
+ }
234
+ function toCounts(value, op, field) {
235
+ if (!isRecord(value) || Object.values(value).some((n) => typeof n !== 'number')) {
236
+ throw new CivitaiError(`storage ${op}: malformed \`${field}\``);
237
+ }
238
+ return { ...value };
239
+ }
240
+ function toDocument(reply, op) {
241
+ if (!isRecord(reply) || typeof reply.key !== 'string' || !isRecord(reply.data) || typeof reply.version !== 'number') {
242
+ throw new CivitaiError(`storage ${op}: reply is not a document`);
243
+ }
244
+ const viewer = reply.viewer;
245
+ if (viewer !== null &&
246
+ !(isRecord(viewer) && Array.isArray(viewer.marks) && viewer.marks.every((m) => typeof m === 'string'))) {
247
+ throw new CivitaiError(`storage ${op}: malformed \`viewer\``);
248
+ }
249
+ return {
250
+ key: reply.key,
251
+ data: reply.data,
252
+ version: reply.version,
253
+ createdAt: toDate(reply.createdAt, op, 'createdAt'),
254
+ updatedAt: toDate(reply.updatedAt, op, 'updatedAt'),
255
+ author: toAuthor(reply.author, op),
256
+ counters: toCounts(reply.counters, op, 'counters'),
257
+ marks: toCounts(reply.marks, op, 'marks'),
258
+ viewer: viewer === null ? null : { marks: [...viewer.marks] },
259
+ };
260
+ }
@@ -29,13 +29,30 @@ export function createHttp(options) {
29
29
  const { session } = options;
30
30
  let response = await send(method, path, opts, await session.getToken({ signal: opts.signal }));
31
31
  // One retry: a token that expired in flight is routine, a second refusal is not.
32
- if (response.status === 401) {
32
+ // Not when the server says the VIEWER must sign in — a fresh token for the same
33
+ // anonymous viewer is refused identically, so the retry would only cost a
34
+ // re-mint and a second request.
35
+ if (response.status === 401 && !(await refusesTheViewer(response))) {
33
36
  const fresh = await session.getToken({ fresh: true, signal: opts.signal });
34
37
  response = await send(method, path, opts, fresh);
35
38
  }
36
39
  return read(response);
37
40
  };
38
41
  }
42
+ /**
43
+ * A 401 about the viewer rather than the token: the storage routes' envelope code
44
+ * `sign_in_required`. Read from a clone, so the body is still there for `read`. A
45
+ * body that cannot be read is treated as a token refusal, i.e. retried as before.
46
+ */
47
+ async function refusesTheViewer(response) {
48
+ try {
49
+ const body = (await response.clone().json());
50
+ return body?.code === 'sign_in_required';
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
39
56
  function urlFor(baseUrl, path, query) {
40
57
  const url = new URL(`${baseUrl}/${path.replace(/^\/+/, '')}`);
41
58
  for (const [key, value] of Object.entries(query ?? {})) {
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 { StorageError } from './doc-store/index.js';
15
+ export type { DocumentAuthor, DocumentCallOptions, DocumentListQuery, DocumentPage, DocumentStorage, DocumentWhereValue, StorageCollection, StorageCollectionInfo, StorageDocument, StorageErrorCode, StorageIndexType, StorageInfo, StorageLimits, StorageUsage, WriteOptions, } from './doc-store/index.js';
14
16
  export { SubmissionUnconfirmedError } from './workflows/index.js';
15
17
  export type { WorkflowCallOptions, WorkflowGetOptions, WorkflowListQuery, WorkflowListResult, WorkflowSubmitOptions, WorkflowWatchOptions, WorkflowsClient, } from './workflows/index.js';
16
18
  export type { AiToolkitLrScheduler, AiToolkitOptimizerType, AiToolkitPlainEcosystem, AiToolkitTrainingParams, AiToolkitTrainingParamsBase, AppWorkflow, AppWorkflowImage, BlockModel3dOutput, BlockModelSubstitution, BlockPublishedModel, BlockSourceImage, BlockStepQuote, BlockStepToolCall, BlockTextToImageParams, BlockTrainedEpoch, BlockTrainingQuote, BlockWorkflowSnapshot, BlockWorkflowStepSnapshot, BuzzAccountType, InlineComfyNode, KnownBlockWorkflowErrorCode, ModelSubstitutionReason, WithWorkflowStepRefs, WorkflowBody, WorkflowBodyCustomComfy, WorkflowBodyCustomComfyInline, WorkflowBodyCustomComfyRecipe, WorkflowBodyPassThroughStep, WorkflowBodyStep, WorkflowBodyTextToImage, WorkflowBodyTraining, WorkflowBodyWorkflow, WorkflowBodyWorkflowStep, WorkflowStepRef, } from './workflows/types.generated.js';
@@ -19,7 +21,7 @@ export type { OrchestrationClient, Step, StepTemplate, WaitOptions, Workflow, Wo
19
21
  export { createHost } from './host/index.js';
20
22
  export type { ConsentRefusal, DownloadRequest, Host, HostCallOptions, ImageScanResult, NavigateScope, PendingImage, PickedResource, ResourcePickerType, SourceImage, UploadedImage, } from './host/index.js';
21
23
  export { ApiError } from './http/index.js';
22
- export type { Query, QueryValue, RequestOptions } from './http/index.js';
24
+ export type { Http, Query, QueryValue, RequestOptions } from './http/index.js';
23
25
  export { BridgeError, CivitaiError } from './core/errors.js';
24
26
  export type { BridgeErrorCode, BridgeFailureCode } from './core/errors.js';
25
27
  export { getTransport } from './core/get-transport.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 { StorageError } from './doc-store/index.js';
19
20
  export { SubmissionUnconfirmedError } from './workflows/index.js';
20
21
  export { isTerminal, isTerminalStatus } from './orchestration/index.js';
21
22
  export { createHost } from './host/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,12 +98,18 @@ 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
102
107
  * rejection, never on an empty result.
108
+ *
109
+ * @deprecated The legacy cross-user store. Declare a `shared` collection under
110
+ * `storage.collections` and use `app.storage.collection(name)`, which has counters,
111
+ * marks and authorship built in. This keeps working, unchanged, until the apps that
112
+ * use it have moved.
103
113
  */
104
114
  export interface SharedStorageClient {
105
115
  /**
@@ -57,7 +57,13 @@ export interface StorageQuota {
57
57
  * not act on a partial view branches on the rejection, never on an empty result.
58
58
  */
59
59
  export interface StorageClient {
60
- /** The value, or `null` when the key is unset. Rejects on any refusal. */
60
+ /**
61
+ * The value, or `null` when the key is unset. Rejects on any refusal.
62
+ *
63
+ * @deprecated The legacy per-viewer key/value store. Declare a `private`
64
+ * collection under `storage.collections` and use `app.storage.collection(name)`;
65
+ * this keeps working, unchanged, until the apps that use it have moved.
66
+ */
61
67
  get<T = unknown>(key: string, opts?: StorageCallOptions): Promise<T | null>;
62
68
  /**
63
69
  * Upsert one key. Resolves only when the write landed.
@@ -68,23 +74,43 @@ export interface StorageClient {
68
74
  *
69
75
  * A value of `undefined` is stored as `null`: it does not survive JSON, and
70
76
  * the server writes `null` for an absent value rather than refusing.
77
+ *
78
+ * @deprecated The legacy per-viewer key/value store. Declare a `private`
79
+ * collection under `storage.collections` and use `app.storage.collection(name)`;
80
+ * this keeps working, unchanged, until the apps that use it have moved.
71
81
  */
72
82
  set<T = unknown>(key: string, value: T, opts?: StorageCallOptions): Promise<{
73
83
  ok: true;
74
84
  sizeBytes: number;
75
85
  }>;
76
- /** Idempotent. `deleted: false` means the key was already absent — a success. */
86
+ /**
87
+ * Idempotent. `deleted: false` means the key was already absent — a success.
88
+ *
89
+ * @deprecated The legacy per-viewer key/value store. Declare a `private`
90
+ * collection under `storage.collections` and use `app.storage.collection(name)`;
91
+ * this keeps working, unchanged, until the apps that use it have moved.
92
+ */
77
93
  delete(key: string, opts?: StorageCallOptions): Promise<{
78
94
  ok: true;
79
95
  deleted: boolean;
80
96
  }>;
81
- /** One page of keys, ordered by key ascending. Values are not returned. */
97
+ /**
98
+ * One page of keys, ordered by key ascending. Values are not returned.
99
+ *
100
+ * @deprecated The legacy per-viewer key/value store. Declare a `private`
101
+ * collection under `storage.collections` and use `app.storage.collection(name)`;
102
+ * this keeps working, unchanged, until the apps that use it have moved.
103
+ */
82
104
  list(query?: StorageListQuery, opts?: StorageCallOptions): Promise<StorageListResult>;
83
105
  /**
84
106
  * This viewer's usage against this viewer's caps, in the stored unit.
85
107
  *
86
108
  * It reports neither the key-length cap nor the per-value cap, so a write
87
109
  * that fits the numbers here can still be refused.
110
+ *
111
+ * @deprecated The legacy per-viewer key/value store. Declare a `private`
112
+ * collection under `storage.collections` and use `app.storage.collection(name)`;
113
+ * this keeps working, unchanged, until the apps that use it have moved.
88
114
  */
89
115
  getQuota(opts?: StorageCallOptions): Promise<StorageQuota>;
90
116
  }
package/dist/testing.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { type BlockSnapshot, type BlockTransport } from './core/transport.js';
2
2
  import { type BridgeFailureCode } from './core/errors.js';
3
3
  export { __resetTransport } from './core/get-transport.js';
4
+ export { createFakeStorage, FAKE_STORAGE_MODERATION } from './doc-store/fake.js';
5
+ export type { FakeModerationVerdict, FakeStorage, FakeStorageCall, FakeStorageCaller, FakeStorageOptions, FakeStorageSession, FakeStorageViewer, } from './doc-store/fake.js';
4
6
  export interface FakeTransport extends BlockTransport {
5
7
  /** Every request sent, in order. */
6
8
  readonly sent: {
package/dist/testing.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { EMPTY_SNAPSHOT } from './core/transport.js';
2
2
  import { BridgeError } from './core/errors.js';
3
3
  export { __resetTransport } from './core/get-transport.js';
4
+ export { createFakeStorage, FAKE_STORAGE_MODERATION } from './doc-store/fake.js';
4
5
  /**
5
6
  * An in-memory `BlockTransport` for testing a service against a scripted host.
6
7
  * Replaces driving a full mock host through `postMessage` when the thing under
@@ -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.13.1";
2
+ export declare const VERSION = "0.14.1";
@@ -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.13.1";
3
+ export const VERSION = "0.14.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/sdk",
3
- "version": "0.13.1",
3
+ "version": "0.14.1",
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",