@civitai/sdk 0.17.0 → 0.18.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
@@ -282,6 +282,55 @@ before moderation and before any write budget. An absent field is allowed;
282
282
  ownership is checked only when the document is written. Only phase-one operations exist: no batch writes, change feed, TTL or
283
283
  owner-written documents yet.
284
284
 
285
+ ### In a dev-tunnel preview: local to the tab
286
+
287
+ A dev-tunnel preview (`civitai app dev-tunnel`, the App Assistant's live
288
+ preview) has **no server-side storage**: an app that is not approved has no storage
289
+ namespace, and unreviewed code must not write into a published app's data. So when
290
+ the host marks the frame `storageMode: 'local-preview'` in `BLOCK_INIT`,
291
+ `initialize()` serves `app.storage.collection(name)` and `app.storage.info()` from an
292
+ **in-memory store in this tab** — [`createFakeStorage`](#a-fake-document-store), the
293
+ real client over a seam that enforces the server's rules — and sends **no request**
294
+ to `blocks/storage/*`. Your code does not change; show a badge so nobody mistakes the
295
+ data for real:
296
+
297
+ ```ts
298
+ if (app.storage.mode === 'local-preview') {
299
+ showBadge('Preview data is local to this tab');
300
+ }
301
+ ```
302
+
303
+ - **Your LOCAL manifest decides the collections**, not the server's copy:
304
+ `blockManifestPlugin()` from `@civitai/app-sdk/vite` hands its `scopes` and
305
+ `storage` to the page on the dev server (never in a build); that is the only way
306
+ the manifest reaches the SDK. It does so from the `@civitai/app-sdk` release that
307
+ includes this change (the one whose CHANGELOG entry reads "`blockManifestPlugin()`
308
+ exposes the manifest's `scopes` and `storage` to `@civitai/sdk` on the dev server
309
+ only"); a `^0.x` range never moves to a newer minor, so raise that pin by hand.
310
+ Not every scaffold registers the plugin (the CLI's `page-money` template does
311
+ not); add it there. Without it, every document-store call rejects
312
+ `local_preview_no_manifest` (a `StorageError`, status 404, sent nowhere); a
313
+ declaration the local store cannot serve rejects `local_preview_invalid_manifest`
314
+ (also 404), and a local store that failed to load rejects
315
+ `local_preview_unavailable` (also 404; reload the preview). None is a status this
316
+ SDK treats as retryable, and none falls back to the real routes.
317
+ - **Model-version ownership is not checked locally.** A collection's
318
+ `ownedModelVersions` fields accept any well-formed model version id in a preview;
319
+ the server checks that the writer owns it. A malformed value is still refused.
320
+ - **One viewer, one tab, gone on reload.** It cannot show other people's entries,
321
+ real vote totals, ranking across users, the minimum-trust gate on shared writes,
322
+ or moderation of real content.
323
+ - **Only the document store.** The deprecated `app.storage.get/set/…`,
324
+ `app.sharedStorage` and `@civitai/blocks-react`'s `useAppStorage` /
325
+ `useSharedStorage` keep calling the real routes, which refuse a preview.
326
+ - **Only on the host's signal.** `app.storage.mode` is latched when `initialize()`
327
+ resolves, and never inferred from the token: a published app whose manifest forgot
328
+ `apps:storage` holds a token that looks the same, and keeps getting the real
329
+ route's `insufficient_scope`. That refusal's message says what to do —
330
+ declare `apps:storage` under `scopes`; it is granted once the app is approved — and
331
+ that in a preview this SDK would have kept the data locally had the host marked the
332
+ frame.
333
+
285
334
  The legacy stores below keep working, unchanged, while apps move; their methods are
286
335
  `@deprecated`. Nothing is copied from them into collections.
287
336
 
@@ -9,6 +9,21 @@ import { type SiteClient } from '../site/index.js';
9
9
  import { type StorageClient } from '../storage/index.js';
10
10
  import { type SubListingsClient } from '../sub-listings/index.js';
11
11
  import { type WorkflowsClient } from '../workflows/index.js';
12
+ /**
13
+ * Which store serves `app.storage.collection(name)` and `app.storage.info()`.
14
+ *
15
+ * - `'remote'` — the server's document store, `/api/v1/blocks/storage/…`.
16
+ * - `'local-preview'` — an in-memory store in this tab, because the host marked
17
+ * the frame a dev-tunnel preview (which has no server-side storage). It
18
+ * enforces the server's rules against the LOCAL manifest, models ONE viewer,
19
+ * and is gone on reload. Show the viewer a "preview data is local to this tab"
20
+ * badge.
21
+ *
22
+ * Latched when `initialize()` resolves. Only the document store switches: the
23
+ * deprecated key/value methods on `app.storage`, `app.sharedStorage` and the
24
+ * `@civitai/blocks-react` storage hooks keep calling the real routes.
25
+ */
26
+ export type StorageMode = 'remote' | 'local-preview';
12
27
  export interface AppClient {
13
28
  /**
14
29
  * The public Civitai REST API (`/api/v1`), as the viewer.
@@ -36,7 +51,9 @@ export interface AppClient {
36
51
  * with an OAuth access token is refused. The legacy store refuses an anonymous
37
52
  * viewer outright; the document store lets one read SHARED collections only.
38
53
  */
39
- readonly storage: StorageClient & DocumentStorage;
54
+ readonly storage: StorageClient & DocumentStorage & {
55
+ readonly mode: StorageMode;
56
+ };
40
57
  /**
41
58
  * @deprecated Declare a `shared` collection under `storage.collections` and use
42
59
  * `app.storage.collection(name)`. This store keeps working, unchanged, until the
package/dist/app/index.js CHANGED
@@ -6,6 +6,7 @@ import { createOrchestrationClient, DEFAULT_ORCHESTRATION_URL, } from '../orches
6
6
  import { createTokenSession, } from '../session/index.js';
7
7
  import { createSharedStorageClient, } from '../shared-storage/index.js';
8
8
  import { createDocumentStorage } from '../doc-store/index.js';
9
+ import { createLocalPreviewStorage, readInjectedManifest } from '../doc-store/local-preview.js';
9
10
  import { createSiteClient, DEFAULT_SITE_URL } from '../site/index.js';
10
11
  import { createStorageClient } from '../storage/index.js';
11
12
  import { createSubListingsClient } from '../sub-listings/index.js';
@@ -21,8 +22,17 @@ export async function initialize(options = {}) {
21
22
  const holdsBlockTokenNow = holdsBlockToken(snapshot);
22
23
  // Read live, like the predicate above: the host rotates the token.
23
24
  const holdsOAuthTokenNow = () => snapshot().token.kind === 'oauth';
25
+ // 🔴 Latched here, ONCE, and only on the host's exact signal — never inferred
26
+ // from `token.scopes`. A published app whose manifest forgot `apps:storage`
27
+ // holds a token that looks the same as a preview's, and must keep getting the
28
+ // real route's `insufficient_scope` rather than a silent local store.
29
+ const local = snapshot().storageMode === 'local-preview'
30
+ ? // The manifest arrives only from `blockManifestPlugin()`'s dev-server
31
+ // global: there is deliberately no option to pass one.
32
+ await createLocalPreviewStorage(readInjectedManifest(), snapshot().viewer)
33
+ : undefined;
24
34
  return {
25
- ...createAppClient(createHostSession(transport), options, holdsBlockTokenNow, holdsOAuthTokenNow),
35
+ ...createAppClient(createHostSession(transport), options, holdsBlockTokenNow, holdsOAuthTokenNow, local),
26
36
  host: createHost(transport),
27
37
  get viewer() {
28
38
  return snapshot().viewer;
@@ -81,7 +91,9 @@ const OAUTH_OPT_IN = 'declare `auth: "oauth"` in block.manifest.json';
81
91
  * refuses, with nothing in this repo able to notice it going stale.
82
92
  */
83
93
  const BLOCK_NAMESPACE = /^\/*blocks\//;
84
- function createAppClient(session, options, holdsBlockTokenNow = () => false, holdsOAuthTokenNow = () => false) {
94
+ function createAppClient(session, options, holdsBlockTokenNow = () => false, holdsOAuthTokenNow = () => false,
95
+ /** A local preview's document store; when present, the site HTTP client never reaches the document store. */
96
+ localDocuments) {
85
97
  const http = (baseUrl) => createHttp({ session, baseUrl, fetch: options.fetch });
86
98
  // One instance for every site-hosted surface: the storage, sub-listings and
87
99
  // workflows clients' routes live under the same base URL, so a `siteUrl`
@@ -100,7 +112,15 @@ function createAppClient(session, options, holdsBlockTokenNow = () => false, hol
100
112
  // One object, two stores, no shared names: the legacy five and the document
101
113
  // store's `collection`/`info`. Spread rather than chained so neither keeps a
102
114
  // reference to the other.
103
- storage: { ...createStorageClient(siteHttp), ...createDocumentStorage(siteHttp) },
115
+ //
116
+ // In a local preview the document store is the in-memory one and is never
117
+ // handed `siteHttp`: zero `blocks/storage/*` requests by construction. The
118
+ // legacy key/value methods are out of scope and keep the real routes.
119
+ storage: {
120
+ ...createStorageClient(siteHttp),
121
+ ...(localDocuments ?? createDocumentStorage(siteHttp, { explainInsufficientScope: true })),
122
+ mode: localDocuments ? 'local-preview' : 'remote',
123
+ },
104
124
  sharedStorage: createSharedStorageClient(siteHttp),
105
125
  subListings: createSubListingsClient(siteHttp),
106
126
  workflows: createWorkflowsClient(explainWorkflowRefusal(siteHttp, holdsOAuthTokenNow)),
@@ -98,7 +98,17 @@ export interface BlockInitPayload {
98
98
  maxBrowsingLevel?: number;
99
99
  /** The domain ceiling narrowed to this viewer. Always a subset of the above. */
100
100
  effectiveBrowsingLevel?: number;
101
+ /**
102
+ * `'local-preview'` when the host mounted this frame as a dev-tunnel preview,
103
+ * which has no server-side storage. The host sets it from the surface it
104
+ * mounted, never from the token or anything the block sent. Absent everywhere
105
+ * else, and from a host that predates it. Typed `unknown` on the wire: only
106
+ * the exact string switches anything — see {@link LOCAL_PREVIEW_STORAGE}.
107
+ */
108
+ storageMode?: unknown;
101
109
  }
110
+ /** The one value of `BLOCK_INIT.storageMode` the SDK acts on. Compared exactly. */
111
+ export declare const LOCAL_PREVIEW_STORAGE = "local-preview";
102
112
  /**
103
113
  * Host messages the transport itself acts on. Domains own everything else.
104
114
  *
@@ -2,6 +2,8 @@
2
2
  * The host's side of the handshake, as it arrives on the wire. The shapes are
3
3
  * the host's and must track it.
4
4
  */
5
+ /** The one value of `BLOCK_INIT.storageMode` the SDK acts on. Compared exactly. */
6
+ export const LOCAL_PREVIEW_STORAGE = 'local-preview';
5
7
  /** Discriminator only — the payload is still unvalidated wire data. */
6
8
  export function isHostMessage(data, type) {
7
9
  return (typeof data === 'object' &&
@@ -29,6 +29,12 @@ export interface BlockSnapshot {
29
29
  maxBrowsingLevel?: number;
30
30
  /** The domain ceiling narrowed to this viewer. Always a subset of `maxBrowsingLevel`. */
31
31
  effectiveBrowsingLevel?: number;
32
+ /**
33
+ * `'local-preview'` when the host said this frame is a dev-tunnel preview with
34
+ * no server-side storage; absent otherwise. `initialize()` reads it ONCE, so a
35
+ * later change cannot move an app's storage between stores mid-session.
36
+ */
37
+ storageMode?: 'local-preview';
32
38
  }
33
39
  export interface RequestOptions {
34
40
  signal?: AbortSignal;
@@ -1,3 +1,4 @@
1
+ import { LOCAL_PREVIEW_STORAGE } from './handshake.js';
1
2
  export const EMPTY_SNAPSHOT = {
2
3
  ready: false,
3
4
  renderMode: 'iframe',
@@ -24,6 +25,9 @@ export function snapshotFromInit(payload, hostOrigin) {
24
25
  domain: payload.domain,
25
26
  maxBrowsingLevel: payload.maxBrowsingLevel,
26
27
  effectiveBrowsingLevel: payload.effectiveBrowsingLevel,
28
+ // The exact string or nothing: a value this SDK does not know must read as
29
+ // "not a local preview", which keeps today's behaviour (the real routes).
30
+ ...(payload.storageMode === LOCAL_PREVIEW_STORAGE ? { storageMode: LOCAL_PREVIEW_STORAGE } : {}),
27
31
  };
28
32
  }
29
33
  /** Wire `WrappedToken` (ISO expiry) → runtime `BlockToken` (Date expiry). */
@@ -171,7 +171,16 @@ 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' | '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 & {});
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'
175
+ /**
176
+ * LOCAL, never sent by the server: the host marked this frame a dev-tunnel
177
+ * local preview, and no manifest reached the SDK to say which collections exist.
178
+ */
179
+ | 'local_preview_no_manifest'
180
+ /** LOCAL, never sent by the server: the local preview's manifest declares storage the local store cannot serve. */
181
+ | 'local_preview_invalid_manifest'
182
+ /** LOCAL, never sent by the server: the local preview's in-memory store could not be loaded. */
183
+ | 'local_preview_unavailable' | (string & {});
175
184
  /**
176
185
  * A refusal from the document store, in its one envelope `{ code, message, error, details? }`.
177
186
  * Branch on `code`; `status` and `body` are {@link ApiError}'s, as sent.
@@ -184,4 +193,16 @@ export declare class StorageError extends ApiError {
184
193
  /** Seconds to wait before retrying, when the server named one (`rate_limited`, a retryable 503). */
185
194
  get retryAfter(): number | undefined;
186
195
  }
187
- export declare function createDocumentStorage(http: Http): DocumentStorage;
196
+ /**
197
+ * Appended to a REAL route's `insufficient_scope`, after the server's own sentence.
198
+ * The server cannot tell a published app whose manifest forgot the scope from a
199
+ * dev-tunnel preview (which is never granted it), so the message covers both —
200
+ * and says that, in a preview, this SDK keeps the data locally once the host
201
+ * marks the frame, so seeing this refusal there means the host did not.
202
+ */
203
+ export declare const INSUFFICIENT_SCOPE_HINT: string;
204
+ export interface DocumentStorageOptions {
205
+ /** Append {@link INSUFFICIENT_SCOPE_HINT} to an `insufficient_scope` refusal. The server's `code` is unchanged. */
206
+ explainInsufficientScope?: boolean;
207
+ }
208
+ export declare function createDocumentStorage(http: Http, options?: DocumentStorageOptions): DocumentStorage;
@@ -42,8 +42,19 @@ export class StorageError extends ApiError {
42
42
  return typeof value === 'number' ? value : undefined;
43
43
  }
44
44
  }
45
+ /**
46
+ * Appended to a REAL route's `insufficient_scope`, after the server's own sentence.
47
+ * The server cannot tell a published app whose manifest forgot the scope from a
48
+ * dev-tunnel preview (which is never granted it), so the message covers both —
49
+ * and says that, in a preview, this SDK keeps the data locally once the host
50
+ * marks the frame, so seeing this refusal there means the host did not.
51
+ */
52
+ export const INSUFFICIENT_SCOPE_HINT = 'Declare `apps:storage` under `scopes` in block.manifest.json; it is granted once the app is approved. ' +
53
+ 'In a dev-tunnel preview, this version of @civitai/sdk keeps preview data in a local in-memory store ' +
54
+ "instead, when the host marks the frame `storageMode: 'local-preview'` — this refusal came from the real " +
55
+ 'route, so this frame was not marked.';
45
56
  /** An {@link ApiError} whose body is the store's envelope becomes a {@link StorageError}; anything else passes through. */
46
- function toStorageError(error) {
57
+ function toStorageError(error, options = {}) {
47
58
  if (!(error instanceof ApiError) || error instanceof StorageError)
48
59
  return error;
49
60
  const body = error.body;
@@ -52,7 +63,10 @@ function toStorageError(error) {
52
63
  const { code, message, details } = body;
53
64
  if (typeof code !== 'string')
54
65
  return error;
55
- return new StorageError(error.status, code, typeof message === 'string' ? message : error.message, body, details != null && typeof details === 'object' && !Array.isArray(details)
66
+ const said = typeof message === 'string' ? message : error.message;
67
+ return new StorageError(error.status, code,
68
+ // `body` stays as the server sent it; only the human-readable sentence grows.
69
+ options.explainInsufficientScope && code === 'insufficient_scope' ? `${said} — ${INSUFFICIENT_SCOPE_HINT}` : said, body, details != null && typeof details === 'object' && !Array.isArray(details)
56
70
  ? details
57
71
  : undefined, { cause: error });
58
72
  }
@@ -87,13 +101,13 @@ function segment(what, value) {
87
101
  }
88
102
  return encodeURIComponent(value);
89
103
  }
90
- export function createDocumentStorage(http) {
104
+ export function createDocumentStorage(http, options = {}) {
91
105
  const call = async (method, path, opts = {}) => {
92
106
  try {
93
107
  return await http(method, path, opts);
94
108
  }
95
109
  catch (error) {
96
- throw toStorageError(error);
110
+ throw toStorageError(error, options);
97
111
  }
98
112
  };
99
113
  return {
@@ -0,0 +1,49 @@
1
+ import type { ViewerInfo } from '../core/handshake.js';
2
+ import { type DocumentStorage } from './index.js';
3
+ /**
4
+ * The document store for a DEV-TUNNEL PREVIEW: the host marked the frame
5
+ * `storageMode: 'local-preview'` (a preview has no server-side storage namespace,
6
+ * by design), so `app.storage.collection(name)` is served by an in-memory store
7
+ * in this tab instead of `/api/v1/blocks/storage/…`.
8
+ *
9
+ * That store is `createFakeStorage` — the real client over a seam that enforces
10
+ * the server's rules — loaded with a dynamic `import()`, so it is a separate
11
+ * chunk that an app outside a preview never downloads. It never receives the
12
+ * site HTTP client, so no request can reach a real route by construction.
13
+ *
14
+ * The collections come from the app's LOCAL manifest, the one being previewed,
15
+ * never from the server. With none available, every call fails with a local
16
+ * error instead of falling through to the real routes or to a permissive store.
17
+ */
18
+ /**
19
+ * Where `@civitai/app-sdk/vite` puts the local manifest's `scopes` and `storage`
20
+ * in a DEV SERVER page (never in a build). Spelled again, on purpose, in
21
+ * `packages/civitai-app-sdk/src/vite/index.ts`; the two copies are pinned equal by
22
+ * `tests/guards/local-preview-manifest-global.test.mjs`.
23
+ */
24
+ export declare const LOCAL_PREVIEW_MANIFEST_GLOBAL = "__CIVITAI_LOCAL_PREVIEW_MANIFEST__";
25
+ /** What a local preview reads from the manifest: the scopes it declares and its `storage` block. */
26
+ export interface LocalPreviewManifest {
27
+ scopes?: readonly string[];
28
+ storage?: unknown;
29
+ }
30
+ /** The manifest the dev server injected, if any. */
31
+ export declare function readInjectedManifest(): LocalPreviewManifest | undefined;
32
+ /**
33
+ * The status of every local error (`local_preview_no_manifest`,
34
+ * `local_preview_invalid_manifest`, `local_preview_unavailable`). A missing or
35
+ * unservable manifest is a SETUP mistake that no wait fixes, so it must not be a status `isTransient`
36
+ * (`src/http/retry.ts`) reads as retryable — that is every 5xx and 429. 404 is
37
+ * the status the server gives the nearest case it has, `collection_not_declared`:
38
+ * with no usable manifest, no collection is declared. `get` still rejects (it
39
+ * reads only the code `not_found` as "no document"), and the `code` tells them
40
+ * apart. A store that failed to load is not retried either: a reload is the fix,
41
+ * and the message says so.
42
+ */
43
+ export declare const LOCAL_PREVIEW_ERROR_STATUS = 404;
44
+ /**
45
+ * Builds the preview's document store. Never throws: a missing or unusable
46
+ * manifest, or a local store that fails to load, yields a store whose every call
47
+ * rejects, so storage cannot stop the rest of the app from starting.
48
+ */
49
+ export declare function createLocalPreviewStorage(manifest: LocalPreviewManifest | undefined, viewer: ViewerInfo | null): Promise<DocumentStorage>;
@@ -0,0 +1,131 @@
1
+ import { createDocumentStorage, StorageError } from './index.js';
2
+ /**
3
+ * The document store for a DEV-TUNNEL PREVIEW: the host marked the frame
4
+ * `storageMode: 'local-preview'` (a preview has no server-side storage namespace,
5
+ * by design), so `app.storage.collection(name)` is served by an in-memory store
6
+ * in this tab instead of `/api/v1/blocks/storage/…`.
7
+ *
8
+ * That store is `createFakeStorage` — the real client over a seam that enforces
9
+ * the server's rules — loaded with a dynamic `import()`, so it is a separate
10
+ * chunk that an app outside a preview never downloads. It never receives the
11
+ * site HTTP client, so no request can reach a real route by construction.
12
+ *
13
+ * The collections come from the app's LOCAL manifest, the one being previewed,
14
+ * never from the server. With none available, every call fails with a local
15
+ * error instead of falling through to the real routes or to a permissive store.
16
+ */
17
+ /**
18
+ * Where `@civitai/app-sdk/vite` puts the local manifest's `scopes` and `storage`
19
+ * in a DEV SERVER page (never in a build). Spelled again, on purpose, in
20
+ * `packages/civitai-app-sdk/src/vite/index.ts`; the two copies are pinned equal by
21
+ * `tests/guards/local-preview-manifest-global.test.mjs`.
22
+ */
23
+ export const LOCAL_PREVIEW_MANIFEST_GLOBAL = '__CIVITAI_LOCAL_PREVIEW_MANIFEST__';
24
+ /** The manifest the dev server injected, if any. */
25
+ export function readInjectedManifest() {
26
+ const value = globalThis[LOCAL_PREVIEW_MANIFEST_GLOBAL];
27
+ return value != null && typeof value === 'object' && !Array.isArray(value)
28
+ ? value
29
+ : undefined;
30
+ }
31
+ const NO_MANIFEST = "local preview storage: the host marked this frame `storageMode: 'local-preview'` (a dev-tunnel preview, " +
32
+ 'which has no server-side storage), and no block.manifest.json reached @civitai/sdk to say which ' +
33
+ 'collections exist. Add `blockManifestPlugin()` from `@civitai/app-sdk/vite` to vite.config and run the ' +
34
+ 'Vite dev server, with the @civitai/app-sdk release that includes the local-preview manifest injection or ' +
35
+ 'later — the plugin does not inject it before that release, and a `^0.x` range never moves to a newer ' +
36
+ 'minor, so raise the pin by hand. Nothing was sent to the server.';
37
+ /**
38
+ * The status of every local error (`local_preview_no_manifest`,
39
+ * `local_preview_invalid_manifest`, `local_preview_unavailable`). A missing or
40
+ * unservable manifest is a SETUP mistake that no wait fixes, so it must not be a status `isTransient`
41
+ * (`src/http/retry.ts`) reads as retryable — that is every 5xx and 429. 404 is
42
+ * the status the server gives the nearest case it has, `collection_not_declared`:
43
+ * with no usable manifest, no collection is declared. `get` still rejects (it
44
+ * reads only the code `not_found` as "no document"), and the `code` tells them
45
+ * apart. A store that failed to load is not retried either: a reload is the fix,
46
+ * and the message says so.
47
+ */
48
+ export const LOCAL_PREVIEW_ERROR_STATUS = 404;
49
+ /**
50
+ * Builds the preview's document store. Never throws: a missing or unusable
51
+ * manifest, or a local store that fails to load, yields a store whose every call
52
+ * rejects, so storage cannot stop the rest of the app from starting.
53
+ */
54
+ export async function createLocalPreviewStorage(manifest, viewer) {
55
+ if (manifest === undefined)
56
+ return refusingStorage('local_preview_no_manifest', NO_MANIFEST);
57
+ let createFakeStorage;
58
+ try {
59
+ ({ createFakeStorage } = await import('./fake.js'));
60
+ }
61
+ catch (error) {
62
+ return refusingStorage('local_preview_unavailable', `local preview storage: the local store could not be loaded — ${error?.message ?? String(error)}. ` +
63
+ 'Reload the preview. Nothing was sent to the server.');
64
+ }
65
+ // MODEL-VERSION OWNERSHIP IS NOT CHECKED LOCALLY. The server checks a write's
66
+ // `ownedModelVersions` fields against the model versions the writer really
67
+ // owns; the local store knows of none, and would refuse every such write the
68
+ // server accepts. So every well-formed id a write carries is added to the
69
+ // viewer's `modelVersions` — the fake's own seam, read per call — before the
70
+ // write reaches it. Malformed values are still refused, as the server refuses them.
71
+ const owned = [];
72
+ const localViewer = viewer === null
73
+ ? null
74
+ : {
75
+ username: viewer.username ?? `user-${viewer.id}`,
76
+ get modelVersions() {
77
+ return owned;
78
+ },
79
+ };
80
+ try {
81
+ const fake = createFakeStorage({
82
+ manifest: { scopes: manifest.scopes, storage: manifest.storage },
83
+ // Latched at init, like the mode: a sign-in remounts the frame.
84
+ viewer: localViewer,
85
+ });
86
+ const http = (method, path, opts) => {
87
+ collectIds(opts?.body, owned);
88
+ return fake.http(method, path, opts);
89
+ };
90
+ return createDocumentStorage(http);
91
+ }
92
+ catch (error) {
93
+ return refusingStorage('local_preview_invalid_manifest', `local preview storage: block.manifest.json declares storage the local preview store cannot serve — ` +
94
+ `${error.message}. Nothing was sent to the server.`);
95
+ }
96
+ }
97
+ /** Every integer a write body carries that could be a model version id. */
98
+ function collectIds(value, into) {
99
+ if (typeof value === 'number') {
100
+ if (Number.isInteger(value) && value >= 1 && !into.includes(value))
101
+ into.push(value);
102
+ }
103
+ else if (Array.isArray(value)) {
104
+ for (const item of value)
105
+ collectIds(item, into);
106
+ }
107
+ else if (value !== null && typeof value === 'object') {
108
+ for (const item of Object.values(value))
109
+ collectIds(item, into);
110
+ }
111
+ }
112
+ /** A store that refuses every call with one LOCAL, non-retryable error. */
113
+ function refusingStorage(code, message) {
114
+ const refuse = () => Promise.reject(new StorageError(LOCAL_PREVIEW_ERROR_STATUS, code, message, { code, message, error: message }));
115
+ // Every method of `StorageCollection`, so `satisfies` fails the build if one is added.
116
+ const collection = {
117
+ get: refuse,
118
+ getMany: refuse,
119
+ put: refuse,
120
+ create: refuse,
121
+ delete: refuse,
122
+ list: refuse,
123
+ increment: refuse,
124
+ mark: refuse,
125
+ unmark: refuse,
126
+ };
127
+ return {
128
+ collection: () => collection,
129
+ info: refuse,
130
+ };
131
+ }
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * inside a civitai.com page, `app.host`. Outside one, `createSignIn()` gets the token.
4
4
  */
5
5
  export { initialize } from './app/index.js';
6
- export type { AppClient, BlockAppClient, BlockInitializeOptions, TokenInitializeOptions, } from './app/index.js';
6
+ export type { AppClient, BlockAppClient, BlockInitializeOptions, StorageMode, TokenInitializeOptions, } from './app/index.js';
7
7
  export { createSignIn, SignInError } from './sign-in/index.js';
8
8
  export type { SignIn, SignInOptions } from './sign-in/index.js';
9
9
  export { SCOPES } from './session/index.js';
@@ -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.17.0";
2
+ export declare const VERSION = "0.18.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.17.0";
3
+ export const VERSION = "0.18.0";
@@ -612,8 +612,14 @@ export type WorkflowBodyPassThroughStep = {
612
612
  *
613
613
  * Consequently the orchestrator's per-`$type` schema is the ONLY authority for
614
614
  * what a given `$type` accepts; nothing in this package or on the host mirrors
615
- * it. Bounded only by size: at most **262144 bytes** (256 KB) serialized, which
616
- * is a payload-DoS bound and not a shape gate.
615
+ * it. Its shape is not checked; its size is: at most **262144 bytes** (256 KB)
616
+ * serialized, which is a payload-DoS bound and not a shape gate.
617
+ *
618
+ * 🔴 **No inline media.** The host refuses a `data:image|video|audio` URI or a
619
+ * long raw base64 string anywhere in here: upload the image, video or audio
620
+ * with `useMediaUpload()` (an image also works with
621
+ * `useImageUpload({ purpose: 'generationSource' })` or `useImagePicker()`)
622
+ * and pass the returned `url`.
617
623
  */
618
624
  input: Record<string, unknown>;
619
625
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/sdk",
3
- "version": "0.17.0",
3
+ "version": "0.18.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",