@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 +49 -0
- package/dist/app/index.d.ts +18 -1
- package/dist/app/index.js +23 -3
- package/dist/core/handshake.d.ts +10 -0
- package/dist/core/handshake.js +2 -0
- package/dist/core/transport.d.ts +6 -0
- package/dist/core/transport.js +4 -0
- package/dist/doc-store/index.d.ts +23 -2
- package/dist/doc-store/index.js +18 -4
- package/dist/doc-store/local-preview.d.ts +49 -0
- package/dist/doc-store/local-preview.js +131 -0
- package/dist/index.d.ts +1 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/workflows/types.generated.d.ts +8 -2
- package/package.json +1 -1
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
|
|
package/dist/app/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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)),
|
package/dist/core/handshake.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/core/handshake.js
CHANGED
|
@@ -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' &&
|
package/dist/core/transport.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/transport.js
CHANGED
|
@@ -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'
|
|
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
|
-
|
|
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;
|
package/dist/doc-store/index.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
2
|
+
export declare 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.
|
|
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