@civitai/app-sdk 0.75.0 → 0.76.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
@@ -32,7 +32,7 @@ pnpm add @civitai/app-sdk
32
32
  | `orchestrator/steps` — `WorkflowStepTemplates`, `WorkflowStepTemplateFor`, `WorkflowStepInputFor`, `AnyWorkflowStepTemplate`, `TypedWorkflowTemplate` | **Type-only** subpath (0 runtime bytes) keying the orchestrator's generated workflow-step shapes from `@civitai/client` by wire `$type`, so apps compose real step bodies against types that track the spec instead of hand-maintained copies. Requires the optional peer `@civitai/client@beta`. Type surface only — it grants no submit permission; see "Typed step shapes" below. |
33
33
  | `blocks/*` — `BlockManifestError`, `BLOCK_SCOPES`, `BLOCK_SCOPE_PATTERN`, `isMessage`, types (`BlockManifestV1`, `BlockContext`, `BlockToken`, `BlockSettings`, `ViewerInfo`, `ThemeInfo`, `BlockWorkflowSnapshot`, `BlockInitPayload`, `ParentToBlockMessage`, `BlockToParentMessage`, …) | Framework-agnostic contract for [Civitai Apps](https://github.com/civitai/civitai-app-starters/blob/main/docs/build-your-first-app-block.md). Ships a byte-identical copy of the server-published canonical JSON Schema (draft 2020-12, https://civitai.com/schemas/app-block/v1.json) at the `./schemas/app-block/v1.json` subpath for offline validation; a CI drift-check keeps it in sync. Runtime-agnostic and **zero runtime dependencies** — no React, no DOM types, nothing in your install graph. Hooks and the iframe transport live in a separate package. |
34
34
  | `manifest/*` — `defineBlock`, `SCHEMA_DIVERGENCES`, `KNOWN_GAPS` (**Node only**) | Build-time manifest validation. `defineBlock(config)` compiles the vendored canonical schema with [Ajv](https://ajv.js.org) and validates a `BlockManifestV1` against it, so authoring mistakes surface in `pnpm dev` instead of at `civitai app validate`/submit. It is **derived from the schema, not a hand-written mirror of it** — that mirror is what produced [#330](https://github.com/civitai/civitai-app-starters/issues/330). Needs `node:fs` and the optional peer `ajv`, which is why it is not on the browser-facing `./blocks` surface. |
35
- | `vite/*` — `blockManifestPlugin` (**Node only**) | A Vite plugin wrapping `defineBlock`, firing from `configResolved` — the one hook Vite calls on both the dev-server and the build path. Every block scaffold registers it, so `pnpm dev`, `pnpm dev:harness` and `pnpm build` all fail on a bad manifest with the offending field path. Optional peers: `ajv` (runtime) and `vite` (types only). |
35
+ | `vite/*` — `blockManifestPlugin` (**Node only**) | A Vite plugin wrapping `defineBlock`, firing from `configResolved` — the one hook Vite calls on both the dev-server and the build path. Every block scaffold registers it, so `pnpm dev`, `pnpm dev:harness` and `pnpm build` all fail on a bad manifest with the offending field path. On the dev server only, it also exposes the manifest's `scopes` + `storage` to `@civitai/sdk` for a dev-tunnel local preview. Optional peers: `ajv` (runtime) and `vite` (types only). |
36
36
 
37
37
  ## Entry points, and what the root barrel is
38
38
 
@@ -407,6 +407,15 @@ import { blockManifestPlugin } from '@civitai/app-sdk/vite';
407
407
  const plugins = [blockManifestPlugin()];
408
408
  ```
409
409
 
410
+ **On the dev server only**, the plugin also puts the manifest's `scopes` and
411
+ `storage` on the page (a `<script>` at the end of `index.html`'s `<head>`; `vite build` never
412
+ emits it). `@civitai/sdk` reads it in a dev-tunnel preview, where the host marks the
413
+ frame `storageMode: 'local-preview'` and `app.storage.collection(name)` is served by
414
+ an in-memory store that enforces these declarations — see "In a dev-tunnel preview"
415
+ in `@civitai/sdk`'s README. Nothing else of the manifest is exposed. Without the
416
+ plugin, a preview's document store refuses every call `local_preview_no_manifest`:
417
+ this plugin is the only way the manifest reaches `@civitai/sdk`.
418
+
410
419
  ### Web storage in a block (`@civitai/app-sdk/safe-storage`)
411
420
 
412
421
  Block iframes are sandboxed **without `allow-same-origin`**, so the document has
@@ -218,6 +218,21 @@ export interface BlockInitPayload {
218
218
  * them — ship without React.
219
219
  */
220
220
  effectiveBrowsingLevel?: number;
221
+ /**
222
+ * `'local-preview'` when the host mounted this frame as a DEV-TUNNEL preview,
223
+ * which has no server-side storage namespace. The host sets it from the
224
+ * surface it mounted — never from the token's scopes, and never from anything
225
+ * the block sent. Absent on every other surface.
226
+ *
227
+ * `@civitai/sdk`'s `initialize()` reads it once and serves
228
+ * `app.storage.collection(name)` from an in-memory store in the tab
229
+ * (`app.storage.mode === 'local-preview'`). Nothing in this package or in
230
+ * `@civitai/blocks-react` acts on it: their storage hooks keep the real routes.
231
+ *
232
+ * ADDITIVE + OPTIONAL. A host that predates it omits the field; a value other
233
+ * than the exact string must be treated as absent.
234
+ */
235
+ storageMode?: 'local-preview';
221
236
  }
222
237
  /**
223
238
  * The freeform value carried by one SHARED-storage entry. Unlike the per-user
@@ -1,4 +1,5 @@
1
1
  import type { Plugin } from 'vite';
2
+ import type { BlockManifest } from '../blocks/types.js';
2
3
  export interface BlockManifestPluginOptions {
3
4
  /**
4
5
  * Path to the manifest. Relative paths resolve against `root`, which defaults
@@ -9,6 +10,34 @@ export interface BlockManifestPluginOptions {
9
10
  /** Overrides the Vite project root used to resolve a relative `manifestPath`. */
10
11
  root?: string;
11
12
  }
13
+ /**
14
+ * The global the dev server's HTML sets for `@civitai/sdk` to read. Spelled again
15
+ * in `packages/civitai-sdk/src/doc-store/local-preview.ts` (the two packages share
16
+ * no code); `tests/guards/local-preview-manifest-global.test.mjs` pins the two equal.
17
+ *
18
+ * WHY A GLOBAL, set by a `<script>` the plugin adds to `index.html`, rather than
19
+ * the alternatives:
20
+ * - a VIRTUAL MODULE needs `@civitai/sdk` to import it, which fails to resolve in
21
+ * every bundle not built by this plugin — production included;
22
+ * - a `define` substitution has to reach `@civitai/sdk` inside `node_modules`,
23
+ * where it depends on how the dependency optimizer pre-bundles it, and it would
24
+ * rewrite production builds too;
25
+ * - an `initialize()` option needs every app to import its manifest and pass it,
26
+ * and would be public API.
27
+ * The `<script>` is added on the DEV SERVER only (`vite build` never emits it),
28
+ * needs no change in an app that registers this plugin, and the SDK reads it only
29
+ * when the host marks the frame a local preview. It is the ONLY way the manifest
30
+ * reaches the SDK, so an app that does NOT register the plugin gets the SDK's
31
+ * `local_preview_no_manifest` error. Every scaffold in this repo that ships a
32
+ * manifest registers it; not every scaffold elsewhere does (the CLI's
33
+ * `page-money` template, for one, does not), and those apps must add it.
34
+ */
35
+ export declare const LOCAL_PREVIEW_MANIFEST_GLOBAL = "__CIVITAI_LOCAL_PREVIEW_MANIFEST__";
36
+ /**
37
+ * The classic `<script>` body: only `scopes` and `storage`, as JSON with `<`
38
+ * escaped so no value can close the element.
39
+ */
40
+ export declare function localPreviewManifestScript(manifest: Pick<BlockManifest, 'scopes' | 'storage'>): string;
12
41
  export declare function blockManifestPlugin(options?: BlockManifestPluginOptions): Plugin;
13
42
  export { BlockManifestError } from '../blocks/manifestError.js';
14
43
  //# sourceMappingURL=index.d.ts.map
@@ -18,6 +18,13 @@
18
18
  * pre-check against the same canonical). It exists so a manifest mistake
19
19
  * surfaces in the dev loop rather than at submit time.
20
20
  *
21
+ * DEV SERVER ONLY, it also hands the manifest's `scopes` and `storage` to
22
+ * `@civitai/sdk`, for a dev-tunnel preview. A preview has no server-side storage,
23
+ * so when the host marks the frame `storageMode: 'local-preview'` the SDK serves
24
+ * `app.storage.collection(name)` from an in-memory store that enforces THESE
25
+ * declarations — the ones of the code being previewed, which the server has
26
+ * never seen. See {@link LOCAL_PREVIEW_MANIFEST_GLOBAL} for why it is a global.
27
+ *
21
28
  * PEERS: `ajv` (used at runtime by `@civitai/app-sdk/manifest`) and `vite`
22
29
  * (types only). Both are optional peers of `@civitai/app-sdk`; a scaffold using
23
30
  * this plugin needs both in its devDependencies.
@@ -25,7 +32,60 @@
25
32
  import { readFileSync } from 'node:fs';
26
33
  import { resolve } from 'node:path';
27
34
  import { BlockManifestError, defineBlock } from '../manifest/index.js';
35
+ /**
36
+ * The global the dev server's HTML sets for `@civitai/sdk` to read. Spelled again
37
+ * in `packages/civitai-sdk/src/doc-store/local-preview.ts` (the two packages share
38
+ * no code); `tests/guards/local-preview-manifest-global.test.mjs` pins the two equal.
39
+ *
40
+ * WHY A GLOBAL, set by a `<script>` the plugin adds to `index.html`, rather than
41
+ * the alternatives:
42
+ * - a VIRTUAL MODULE needs `@civitai/sdk` to import it, which fails to resolve in
43
+ * every bundle not built by this plugin — production included;
44
+ * - a `define` substitution has to reach `@civitai/sdk` inside `node_modules`,
45
+ * where it depends on how the dependency optimizer pre-bundles it, and it would
46
+ * rewrite production builds too;
47
+ * - an `initialize()` option needs every app to import its manifest and pass it,
48
+ * and would be public API.
49
+ * The `<script>` is added on the DEV SERVER only (`vite build` never emits it),
50
+ * needs no change in an app that registers this plugin, and the SDK reads it only
51
+ * when the host marks the frame a local preview. It is the ONLY way the manifest
52
+ * reaches the SDK, so an app that does NOT register the plugin gets the SDK's
53
+ * `local_preview_no_manifest` error. Every scaffold in this repo that ships a
54
+ * manifest registers it; not every scaffold elsewhere does (the CLI's
55
+ * `page-money` template, for one, does not), and those apps must add it.
56
+ */
57
+ export const LOCAL_PREVIEW_MANIFEST_GLOBAL = '__CIVITAI_LOCAL_PREVIEW_MANIFEST__';
58
+ /** Reads and validates the manifest, throwing the message Vite should print. */
59
+ function loadManifest(manifestPath) {
60
+ let manifest;
61
+ try {
62
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
63
+ }
64
+ catch (err) {
65
+ throw new Error(`block.manifest.json could not be read or parsed (${manifestPath}): ${err.message}`);
66
+ }
67
+ try {
68
+ defineBlock({ manifest });
69
+ }
70
+ catch (err) {
71
+ // Re-thrown with the field path up front: Vite prints the message, not
72
+ // the error's own properties, so `.field` would otherwise be invisible.
73
+ const field = err instanceof BlockManifestError && err.field ? ` [${err.field}]` : '';
74
+ throw new Error(`block.manifest.json is invalid${field}: ${err.message}`);
75
+ }
76
+ return manifest;
77
+ }
78
+ /**
79
+ * The classic `<script>` body: only `scopes` and `storage`, as JSON with `<`
80
+ * escaped so no value can close the element.
81
+ */
82
+ export function localPreviewManifestScript(manifest) {
83
+ const exposed = JSON.stringify({ scopes: manifest.scopes, storage: manifest.storage }).replace(/</g, '\\u003c');
84
+ return `globalThis[${JSON.stringify(LOCAL_PREVIEW_MANIFEST_GLOBAL)}] = ${exposed};`;
85
+ }
28
86
  export function blockManifestPlugin(options = {}) {
87
+ let manifestPath = '';
88
+ let serving = false;
29
89
  return {
30
90
  name: 'civitai-block-manifest',
31
91
  // `enforce: 'pre'` so the manifest error is the FIRST thing reported rather
@@ -36,23 +96,27 @@ export function blockManifestPlugin(options = {}) {
36
96
  // this module's own location — this file now lives inside the installed
37
97
  // package, not next to the consumer's manifest.
38
98
  const root = options.root ?? config?.root ?? process.cwd();
39
- const manifestPath = resolve(root, options.manifestPath ?? './block.manifest.json');
40
- let manifest;
41
- try {
42
- manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
43
- }
44
- catch (err) {
45
- throw new Error(`block.manifest.json could not be read or parsed (${manifestPath}): ${err.message}`);
46
- }
47
- try {
48
- defineBlock({ manifest });
49
- }
50
- catch (err) {
51
- // Re-thrown with the field path up front: Vite prints the message, not
52
- // the error's own properties, so `.field` would otherwise be invisible.
53
- const field = err instanceof BlockManifestError && err.field ? ` [${err.field}]` : '';
54
- throw new Error(`block.manifest.json is invalid${field}: ${err.message}`);
55
- }
99
+ manifestPath = resolve(root, options.manifestPath ?? './block.manifest.json');
100
+ serving = config?.command === 'serve';
101
+ loadManifest(manifestPath);
102
+ },
103
+ transformIndexHtml() {
104
+ // Dev server only: a build never carries the manifest into the page.
105
+ if (!serving)
106
+ return undefined;
107
+ // Re-read per page load, so an edited manifest takes effect on reload.
108
+ const manifest = loadManifest(manifestPath);
109
+ return [
110
+ {
111
+ tag: 'script',
112
+ // End of <head>, NOT `head-prepend`: prepended, a large `storage` block
113
+ // could push `<meta charset>` past the 1024-byte prescan. Nothing needs
114
+ // it earlier: the SDK reads the global inside `initialize()`, after
115
+ // BLOCK_INIT, and a classic script still runs before any module script.
116
+ injectTo: 'head',
117
+ children: localPreviewManifestScript(manifest),
118
+ },
119
+ ];
56
120
  },
57
121
  };
58
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/app-sdk",
3
- "version": "0.75.0",
3
+ "version": "0.76.0",
4
4
  "description": "OAuth + PKCE, encrypted-cookie sessions, scopes, and orchestrator helpers for building third-party Civitai apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",