@storypane/sdk 0.0.0-stage → 0.1.0-rc.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Loren Kuich
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,648 @@
1
- # Temporary Holding Version
1
+ # Storypane SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Use `createStorypaneClient` for publications and `useStorypanePreview` for a
4
+ separate preview route. `useStorypaneStory` adds preview to an existing publication.
5
+
6
+ TypeScript SDK for published content and live, read-only draft preview. Works with
7
+ Node, Workers, browsers, Next.js client components, and Astro islands/scripts.
8
+ The delivery and preview entrypoints have no runtime dependencies; React is an optional
9
+ peer dependency. The MCP CLI uses the official MCP server package and Zod.
10
+
11
+ Start with the [Next.js integration guide](https://github.com/lkuich/storypane/blob/main/apps/example-next/README.md) for
12
+ server-side fetching, typed block components, and a separate live preview route.
13
+ An [Astro example](https://github.com/lkuich/storypane/blob/main/apps/example-astro/README.md) is also included.
14
+
15
+ The current version is **0.1.0-rc.2**. Use the SDK tarball attached to the
16
+ [matching GitHub prerelease](https://github.com/lkuich/storypane/releases), or build
17
+ from this monorepo with `pnpm --filter @storypane/sdk build` and pack/link
18
+ `packages/sdk`. npm publication is separately gated on publisher setup; when
19
+ available, select the explicit RC version or `@storypane/sdk@rc`. See the
20
+ [release guide](https://storypane.dev/docs/releases-and-versioning/). Install the release artifact without waiting for npm publication:
21
+
22
+ ```sh
23
+ npm install https://github.com/lkuich/storypane/releases/download/v0.1.0-rc.2/storypane-sdk-0.1.0-rc.2.tgz
24
+ ```
25
+
26
+ Entrypoints:
27
+
28
+ - `@storypane/sdk`: delivery client, types, preview detection, editable attributes.
29
+ - `@storypane/sdk/preview`: framework-independent browser subscription and editor bridge.
30
+ - `@storypane/sdk/react`: `useStorypanePreview`, `useStorypaneStory`, and `editable`.
31
+ - `@storypane/sdk/rich-text`: `RichText`, a safe React renderer that works in Server and Client Components.
32
+
33
+ ## Configure the CMS
34
+
35
+ For a team, start with [production for editors and a linked dev space per developer](https://github.com/lkuich/storypane/blob/main/docs/development-workflow.md).
36
+ Use production credentials and production-generated types for the live website,
37
+ and a separate dev configuration for schema and frontend testing.
38
+
39
+ In the selected space’s **Settings → API & preview**, create a **Published content** token and
40
+ set the application preview URL, e.g. `https://site.example/{locale}/{slug}`.
41
+ The root locale contributes an empty path segment; the editor removes doubled
42
+ slashes. `{slug}` is the selected locale's URL slug. Query placeholders also work:
43
+ `https://site.example/preview?slug={slug}&locale={locale}`.
44
+
45
+ The default preview parameter is `storypane_preview=1`. You can change its name in
46
+ settings; use the same name in your SDK options. Your app must permit the CMS
47
+ origin in its CSP `frame-ancestors` policy and must not send a conflicting
48
+ `X-Frame-Options` header. An HTTPS CMS requires an HTTPS app; localhost HTTP is
49
+ supported for development.
50
+
51
+ ## Published content
52
+
53
+ ```ts
54
+ import { createStorypaneClient, type StorypaneBlock } from '@storypane/sdk';
55
+ import { storypaneMigrationVersion } from './types/storypane';
56
+
57
+ const storypane = createStorypaneClient({
58
+ url: process.env.STORYPANE_URL!,
59
+ spaceId: process.env.STORYPANE_SPACE_ID!,
60
+ token: process.env.STORYPANE_DELIVERY_TOKEN!,
61
+ migrationVersion: storypaneMigrationVersion,
62
+ });
63
+
64
+ const page = await storypane.getStory({ slug: 'home' });
65
+ const byId = await storypane.getStory({ id: page.id });
66
+ const { stories, nextOffset } = await storypane.listStories();
67
+ const allStories = await storypane.listAllStories();
68
+ const locales = await storypane.listLocales(); // [{ id, name: 'Default', slug: '', isDefault: true }, ...]
69
+ ```
70
+
71
+ Locale is optional. These calls use the space's configured default locale;
72
+ `listStories()` and `listAllStories()` need no options object. A single-story read
73
+ still needs its ID or slug. Pass `{ locale: 'fr' }` to either list method, or add
74
+ `locale: 'fr'` to a story reference, when you want a specific locale.
75
+ You can also set pagination without a locale: `listStories({ limit: 100 })`.
76
+ An explicit `locale: ''` selects the root locale, even if another locale is the
77
+ configured default. Changing the default does not change URL prefixes.
78
+
79
+ These reads use the CMS delivery API, which queries publication snapshots in
80
+ Postgres. Drafts and unpublished locales never appear. A missing publication in
81
+ the selected locale does not fall back to another locale; a single-story read
82
+ throws `StorypaneError` with `status === 404`.
83
+ Each content block includes `_uid` and `component`; nested block fields contain
84
+ arrays. Schema-specific generics are supported: `storypane.getStory<MyPage>(...)`.
85
+
86
+ `listLocales()` reads the names and prefixes configured in **Space settings →
87
+ Locales** using a published or preview token. It exposes no draft content. Use
88
+ these slugs with `getStory` / `listStories` to build routes; locales with no
89
+ published stories return an empty list. Slug changes preserve content IDs but
90
+ require updating old links and rebuilding static routes.
91
+
92
+ API responses are `no-store` so token revocation and new publications take effect
93
+ on the next request. Your app can deliberately cache/build the published response.
94
+ SSG pages still need a rebuild or framework revalidation after publishing. Preview
95
+ does not modify their static HTML or production cache.
96
+
97
+ ## Next.js App Router
98
+
99
+ Keep the client in a module marked `import 'server-only'`. Call `getStory` from
100
+ an async Server Component, then pass the content to your own React components.
101
+ The client accepts a schema generic once; individual methods can still override
102
+ it with `getStory<MyPage>(...)`.
103
+
104
+ ```ts
105
+ // src/lib/storypane.ts
106
+ import 'server-only';
107
+ import { createStorypaneClient } from '@storypane/sdk';
108
+ import { storypaneMigrationVersion, type StorypaneContentType } from '@/types/storypane';
109
+
110
+ export const storypane = createStorypaneClient<StorypaneContentType>({
111
+ url: process.env.STORYPANE_URL!,
112
+ spaceId: process.env.STORYPANE_SPACE_ID!,
113
+ token: process.env.STORYPANE_DELIVERY_TOKEN!,
114
+ migrationVersion: storypaneMigrationVersion,
115
+ });
116
+ ```
117
+
118
+ ```tsx
119
+ // src/app/about/page.tsx
120
+ import { storypane } from '@/lib/storypane';
121
+
122
+ export default async function Page() {
123
+ const story = await storypane.getStory({ slug: 'about' });
124
+ return <h1>{story.name}</h1>;
125
+ }
126
+ ```
127
+
128
+ For static routes, `listAllStories()` returns every published story in the
129
+ configured default locale, following pagination. To build routes for every locale,
130
+ call `listLocales()` and fetch the others with `listAllStories({ locale: slug })`.
131
+ Each locale includes `isDefault`; the default need not have an empty URL prefix.
132
+ `getStory`, `listStories`, and `listAllStories` accept fetch options as their second
133
+ argument. Choose static rendering or request-time fetching in your Next.js routes;
134
+ the SDK does not impose a cache policy on story reads.
135
+
136
+ ### A separate preview route
137
+
138
+ Use `useStorypanePreview` in a Client Component when no initial publication is
139
+ available. It returns `{ story, status, error }`; `story` stays undefined until
140
+ the editor authorizes the connection. Pass an undefined story reference until
141
+ your route has a slug. Without a parent editor or authenticated `getSession`
142
+ callback, the hook stays disabled.
143
+
144
+ ```tsx
145
+ 'use client';
146
+ import { useStorypanePreview } from '@storypane/sdk/react';
147
+ import { storypaneMigrationVersion, type StorypaneContentType } from '@/types/storypane';
148
+
149
+ export function Preview({
150
+ cmsUrl,
151
+ spaceId,
152
+ slug,
153
+ locale = '',
154
+ }: {
155
+ cmsUrl: string;
156
+ spaceId: string;
157
+ slug: string;
158
+ locale?: string;
159
+ }) {
160
+ const { story, error } = useStorypanePreview<StorypaneContentType>({
161
+ cmsUrl,
162
+ spaceId,
163
+ migrationVersion: storypaneMigrationVersion,
164
+ story: { slug, locale },
165
+ });
166
+ if (error) return <p role="alert">{error.message}</p>;
167
+ if (!story) return <p>Open this page from Storypane's preview.</p>;
168
+ return <h1>{story.name}</h1>; // Render your block components here.
169
+ }
170
+ ```
171
+
172
+ The [complete Next.js example](https://github.com/lkuich/storypane/blob/main/apps/example-next/README.md) includes routing,
173
+ `Suspense` around `useSearchParams`, block rendering, and referenced footer drafts.
174
+ It keeps published pages on the server and reuses the renderer inside `/preview`.
175
+ Client preview components must not import server-only fetch modules.
176
+
177
+ If you prefer preview on an existing published route, `useStorypaneStory(initial,
178
+ options)` remains available. It starts with the publication and activates drafts
179
+ only when its `mode` matches. For Next.js Draft Mode, pass the server's `isEnabled`
180
+ value as `mode.draft`; never read the HttpOnly draft cookie from browser JavaScript.
181
+ An arbitrary Server Component cannot be re-rendered by changing browser data;
182
+ the live region must have a client-compatible renderer.
183
+
184
+ `editable(block)` supplies selection attributes. Put it on each block's wrapper;
185
+ focusable wrappers also support Enter/Space selection in the editor. The SDK
186
+ cleans up subscriptions, listeners, and highlighting on unmount.
187
+
188
+ ### Rich text
189
+
190
+ ```tsx
191
+ import { RichText } from '@storypane/sdk/rich-text';
192
+
193
+ <RichText value={block.text} />;
194
+ ```
195
+
196
+ The renderer supports the editor's nodes and marks, escapes text, and rejects
197
+ unsafe link protocols. It also accepts legacy plain strings and empty draft
198
+ values. It has no client-only hooks, so it works in both Server Components and
199
+ live previews. It does not inject CMS strings as HTML.
200
+
201
+ ## Astro
202
+
203
+ Render publications at build time with `@astrojs/react`, without a hydration
204
+ directive. The example's app-owned `publishedPages` helper fetches every configured
205
+ locale through `listAllStories`, validates routes, and supplies the selected story
206
+ and its references:
207
+
208
+ ```astro
209
+ ---
210
+ import { Content } from '../components/content';
211
+ import { publishedPages } from '../lib/cms';
212
+ export const getStaticPaths = publishedPages;
213
+ const { story, references } = Astro.props;
214
+ ---
215
+
216
+ <Content block={story.content} references={references} />
217
+ ```
218
+
219
+ Keep the delivery token in server-only environment variables. Public pages have
220
+ no draft subscription. Put the interactive preview on a separate `/preview` route:
221
+
222
+ ```astro
223
+ ---
224
+ import PreviewShell from '../components/PreviewShell';
225
+ import { cmsUrl, spaceId } from '../lib/cms';
226
+ ---
227
+
228
+ <PreviewShell client:load cmsUrl={cmsUrl} spaceId={spaceId} />
229
+ ```
230
+
231
+ The example's [`PreviewShell`](https://github.com/lkuich/storypane/blob/main/apps/example-astro/src/components/PreviewShell.tsx)
232
+ uses `useStorypanePreview` with the URL's `slug` and `locale`, clears stale content
233
+ on navigation, and renders the references supplied by the authenticated editor.
234
+ It also previews stories that have never been published. Configure the CMS preview
235
+ URL as `/preview?slug={slug}&locale={locale}`; no public environment flag is needed.
236
+
237
+ Without React, use a bundled Astro `<script>` and render the subscribed content:
238
+
239
+ ```ts
240
+ import { connectPreview } from '@storypane/sdk/preview';
241
+ import { storypaneMigrationVersion } from './types/storypane';
242
+ const stop = connectPreview({
243
+ cmsUrl: 'https://cms.example',
244
+ spaceId: 'your-space-id',
245
+ migrationVersion: storypaneMigrationVersion,
246
+ story: { slug: 'home' },
247
+ mode: { query: { name: 'storypane_preview', value: '1' } },
248
+ onChange: (story) => renderWithYourClientComponents(story.content),
249
+ });
250
+ // Call stop() when the page/island unmounts, including Astro client navigation.
251
+ ```
252
+
253
+ Pure `.astro` components are server/build-time templates. A browser script must
254
+ update their DOM, or a hydrated island must render the live region. The SDK does
255
+ not inject HTML or guess how your custom components should render.
256
+
257
+ ## Standalone preview and authorization
258
+
259
+ Any of these configured signals activates preview: `mode.draft === true`, a true
260
+ env flag (`true`, `'true'`, or `'1'`), a matching cookie, or a matching query param.
261
+ Signals are ORed. Use `query:false` to disable the default URL trigger. A rule
262
+ without `value` checks for presence. HttpOnly cookies require a server-derived
263
+ `draft` boolean. Build-time browser env vars require a rebuild when changed.
264
+
265
+ Inside the CMS iframe, the SDK requests a session from its parent with
266
+ `postMessage`; both sides check the exact origin and source window. The editor
267
+ uses its authenticated session to grant five minutes of read-only access to one
268
+ story locale. The SDK renews that grant while the editor remains authorized.
269
+ Third-party cookies are not needed in the iframe, and credentials are not added
270
+ to the iframe URL. The browser receives the story's authorized preview over a
271
+ WebSocket.
272
+
273
+ Outside the iframe, provide `getSession`. Your app's authenticated server endpoint
274
+ must verify the visitor may preview before using a **Preview · server only** token:
275
+
276
+ ```ts
277
+ // YOUR APP SERVER: only after checking your editor session/authorization.
278
+ const previewClient = createStorypaneClient({
279
+ url: process.env.STORYPANE_URL!,
280
+ spaceId: process.env.STORYPANE_SPACE_ID!,
281
+ token: process.env.STORYPANE_PREVIEW_TOKEN!,
282
+ });
283
+ const session = await previewClient.createPreviewSession({ slug: 'home' });
284
+ // Return session as JSON with Cache-Control: no-store.
285
+ ```
286
+
287
+ `createPreviewSession` also uses the configured default when you omit `locale`.
288
+ Include it in the story reference to request a specific locale.
289
+
290
+ ```ts
291
+ // In connectPreview(...) or useStorypaneStory(...) options:
292
+ getSession: async () => {
293
+ const response = await fetch('/api/my-authenticated-preview-session', { method: 'POST' });
294
+ if (!response.ok) throw new Error('Sign in to preview');
295
+ return response.json();
296
+ };
297
+ ```
298
+
299
+ Use a stable callback for the React hook (module function or `useCallback`). Your
300
+ endpoint must validate the requested story and CSRF protections appropriate to
301
+ your authentication. A query parameter or an unsigned cookie alone must never
302
+ authorize that endpoint. Never put the long-lived preview token in browser code,
303
+ public env vars, static JSON, or URLs. Only the short-lived session reaches the
304
+ browser. The application origin must match the configured preview URL.
305
+
306
+ Sessions are checked on connect, draft broadcasts, and heartbeats. Revoking a key,
307
+ removing a member, or expiring their login stops subsequent draft delivery.
308
+ Preview sockets accept only sync/heartbeat requests and never hold editing leases.
309
+
310
+ Preview stories include a `references` array for direct story-backed option,
311
+ multi-option, and story-link fields. These contain current drafts from the same
312
+ space, preferring the preview locale with Default as the fallback. References stay
313
+ as IDs in `content`; renderers can look them up in `references`. Referenced-story
314
+ edits refresh on the 15-second heartbeat even if the parent revision is unchanged.
315
+ References are resolved one level deep and are not added to public delivery responses.
316
+ After interruptions the SDK reconnects and reads the latest durable draft; older
317
+ revisions are ignored. Errors are exposed through `onStatus` / the React hook.
318
+
319
+ ## Current limits
320
+
321
+ - Live changes arrive after autosave commits (not on each uncommitted keystroke).
322
+ - Media and link fields retain their structured references. Use `publicAssetUrl`
323
+ for opted-in public media or `client.getAsset` for server-side static export.
324
+ - Component rendering, authentication for standalone preview, application CSP,
325
+ published-site rebuild/revalidation, and unpublished route handling belong to
326
+ the consuming app. The Astro example is built and tested through its real React
327
+ island; the Next.js example remains an integration recipe.
328
+
329
+ ## Management API
330
+
331
+ Use `@storypane/sdk/management` in server code to read and edit drafts, publish
332
+ checkpoints, and manage assets and configuration. Create a scoped token in
333
+ **Space settings → API & preview → Management API**. Tokens and writes obey the
334
+ issuer's current permissions, content restrictions, and active editor locks.
335
+
336
+ ```ts
337
+ import { createManagementClient } from '@storypane/sdk/management';
338
+
339
+ const management = createManagementClient({
340
+ url: process.env.STORYPANE_URL!,
341
+ spaceId: process.env.STORYPANE_SPACE_ID!,
342
+ token: process.env.STORYPANE_MANAGEMENT_TOKEN!,
343
+ });
344
+
345
+ const stories = await management.stories.list({ limit: 20 });
346
+ ```
347
+
348
+ The CLI uses the same environment variables:
349
+
350
+ ```sh
351
+ storypane stories list --json
352
+ storypane content get STORY_ID LOCALE_ID --json
353
+ storypane assets upload ./cover.jpg --mime image/jpeg
354
+ storypane api GET /delivery-settings
355
+ ```
356
+
357
+ See the [management API guide](https://github.com/lkuich/storypane/blob/main/docs/management-api.md) for safe edits,
358
+ publishing, token permissions, pagination, HTTP operations, and CLI JSON input.
359
+ The delivery SDK remains the entrypoint for public sites.
360
+
361
+ ### Content releases
362
+
363
+ Use the management client's existing `request()` method to manage releases:
364
+
365
+ ```ts
366
+ const releases = await management.request('/releases');
367
+ const release = await management.request('/releases', {
368
+ method: 'POST',
369
+ body: {
370
+ requestId: crypto.randomUUID(),
371
+ name: 'Summer launch',
372
+ localTime: '2027-06-01T09:00',
373
+ timezone: 'America/Vancouver',
374
+ dueAt: '2027-06-01T16:00:00.000Z',
375
+ },
376
+ });
377
+ ```
378
+
379
+ Choose a future date; retain the same request ID and payload on retries. Add
380
+ approved page revisions through the scheduling endpoint, using the release ID and
381
+ reviewed revision. See [API contracts](https://github.com/lkuich/storypane/blob/main/docs/management-api.md#content-releases)
382
+ and [the release guide](https://github.com/lkuich/storypane/blob/main/docs/releases.md). Releases publish approved nested
383
+ content together; later drafts stay unpublished. Delivery and preview calls are
384
+ unchanged. No release-specific CLI command is required or provided.
385
+
386
+ ## Schema migrations and generated types
387
+
388
+ A space has one shared active migration. CMS schema saves and CLI pushes update
389
+ that migration; page editing and delivery keep using deployed schemas until an
390
+ admin deploys it. New block types must be deployed before they can be inserted.
391
+
392
+ In your dev space, open **Blocks → Schema migrations → CLI access**. An admin can issue a
393
+ management token with read, stage, or deploy capability. Set `STORYPANE_URL`,
394
+ `STORYPANE_SPACE_ID`, and `STORYPANE_MANAGEMENT_TOKEN` in your terminal or CI secret
395
+ store. Management tokens are never browser credentials. Revocation and the
396
+ issuer's current membership/permissions are checked on every operation.
397
+
398
+ ```sh
399
+ storypane migrations pull
400
+ storypane migrations create "Rename hero title"
401
+ # Edit storypane/migrations/active.json. Keep existing field IDs when renaming.
402
+ storypane migrations push
403
+ # Or review and deploy from an admin's terminal:
404
+ storypane migrations push --deploy
405
+ storypane migrations pull
406
+ storypane generate-types --out src/types/storypane.ts
407
+ ```
408
+
409
+ If the CMS already has an active migration, `pull` downloads it: edit that file
410
+ instead of running `create`. Migration files target exactly one space. Commit the
411
+ `storypane/migrations` directory with application code. Numbered JSON files are
412
+ immutable deployed history; `active.json` is the editable target schema. New
413
+ fields and definitions may omit their IDs; push assigns and persists IDs before
414
+ sending them. Existing IDs preserve renames, including nested blocks. Field order
415
+ in the array controls editor order. Definition API names cannot be renamed. Version 2 migration files also carry
416
+ `parentDefinitionId` beside each definition’s `schema`; preserve these IDs to keep
417
+ the library hierarchy. Version 1 files remain supported and omitted parent metadata
418
+ preserves the current parent. Removing a definition is allowed only when the
419
+ existing content, folder, and schema-reference checks permit it.
420
+
421
+ Local edits and deployed history are never silently overwritten. A stale push or
422
+ divergent pull fails with a conflict. Use `storypane migrations pull --out
423
+ /tmp/storypane-remote` to inspect a separate remote copy, reconcile your changes
424
+ into its active file while preserving its remote revision/source hash, then copy
425
+ that reconciled file into your project. Pull the deployed history too if the base
426
+ version changed. There is no force-push switch.
427
+
428
+ Deployment prints the schema diff and affected draft/publication counts. Deleted
429
+ fields lose their active values; removing a nested-block field removes its
430
+ subtree. Type changes reset values to a valid target default or unset them.
431
+ Renames and plain text format changes preserve values. Defaults do not overwrite
432
+ unchanged fields. Invalid resulting publications, constraints, references, or
433
+ nesting block deployment. Drafts may still omit fields required for publication.
434
+ Interactive deployment requires typing `DEPLOY <version>` when the review
435
+ includes data loss. In the CMS, **Preview destructive deployment** can additionally
436
+ remove disallowed/excess nested blocks and reset invalid field values. It still
437
+ validates the resulting content and requires explicit confirmation. The CLI uses
438
+ standard deployment and does not automatically select this destructive mode.
439
+
440
+ Drafts and live publications migrate separately in one transaction, so deploying
441
+ schemas never publishes unfinished edits. Old values remain in history. Restores
442
+ apply intervening migrations; restore an older checkpoint before publishing it.
443
+
444
+ ### Linked development and production spaces
445
+
446
+ For new teams, create **Production** first for content editors, keep its schema
447
+ read-only, and create a dedicated **Development** space per developer using
448
+ production as the base. Follow the [UI setup and promotion walkthrough](https://github.com/lkuich/storypane/blob/main/docs/development-workflow.md).
449
+ An installation admin can clone a space or choose a base when creating one.
450
+ One production space can have multiple linked dev spaces. Starting in dev is supported: creating
451
+ production from an unlinked dev space establishes the link. New production spaces
452
+ created with an explicit environment start with **Schema read-only** enabled;
453
+ existing spaces retain their previous editing behavior.
454
+
455
+ Cloning takes deployed schemas, block-library nesting, nested-block restrictions,
456
+ locales, and required datasource option lists. It excludes bulk content and media,
457
+ but reviews and copies the stories and original media required by schema defaults,
458
+ including their transitive dependencies (up to 100 stories). Required stories use
459
+ published content where available, otherwise their drafts; copies are always
460
+ unpublished. Memberships, tokens, integrations, and external option request headers
461
+ are not copied. Configure external credentials separately.
462
+
463
+ In **Settings → General**, admins can make schemas read-only. This blocks local
464
+ schema edits, block moves, CLI pushes, and direct deployment. Schema and migration
465
+ history stay visible. Reviewed promotion from a linked dev space is still allowed.
466
+
467
+ Keep `STORYPANE_SPACE_ID` and `STORYPANE_MANAGEMENT_TOKEN` pointed at dev. Set
468
+ `STORYPANE_PRODUCTION_MANAGEMENT_TOKEN` to a separate production token. Sync and
469
+ promotion require a deploy-capable dev token; production needs read access for
470
+ sync and deploy access for promotion. Deploy-capable management tokens need `read`
471
+ and `admin`; add `schemas:write` to the dev token for local migration authoring. Both issuers’ current
472
+ membership and permissions are checked. Production is resolved from the link,
473
+ within the same installation.
474
+
475
+ ```sh
476
+ storypane migrations pull
477
+ storypane migrations create "Add hero caption"
478
+ # Edit storypane/migrations/active.json.
479
+ storypane migrations push --deploy
480
+ # Test the deployed schema in dev, then review actual production impacts.
481
+ storypane migrations status
482
+ storypane migrations promote
483
+ ```
484
+
485
+ Promotion preserves the order of deployed dev migrations and applies the reviewed
486
+ batch in one production transaction. It can copy missing required dependencies as
487
+ unpublished drafts. Dependency copying does not overwrite existing production
488
+ stories or media; schema migrations still transform affected content.
489
+ A required datasource with different option values blocks the transfer until its
490
+ configuration is reconciled explicitly. Active schema drafts are not promoted.
491
+
492
+ Other dev spaces show **Behind production**. Use **Sync from production** in the
493
+ Blocks screen or `storypane migrations sync`. Sync preserves deployed history,
494
+ replays incoming production transformations, and reconciles local changes by block
495
+ and field ID. The UI offers a choice for each conflict. If independent changes
496
+ create invalid nesting or references, choose the complete dev or production schema
497
+ for that step. The CLI prints conflict
498
+ paths; put the choices in a JSON object mapping each path to `"dev"` or `"prod"`,
499
+ then run `storypane migrations sync --input resolutions.json`. Review data loss
500
+ before applying. Unresolved conflicts and stale reviews block application.
501
+
502
+ `--yes` accepts the current review for CI, including reported data loss. The CLI
503
+ keeps an interrupted operation in `storypane/migrations/.transfer.json`; rerun the
504
+ same command to resume it safely. Do not commit this temporary file. A stale
505
+ review is discarded so the next invocation can obtain a new review. Pull dev
506
+ migrations after sync; divergent local active files must still be reconciled using
507
+ a separate `--out` directory.
508
+
509
+ Production and dev migration numbers can differ. Set `PRODUCTION_SPACE_ID` to
510
+ production’s UUID from **Settings → General**. Generate production application
511
+ types from production, keeping its files separate from dev:
512
+
513
+ ```sh
514
+ STORYPANE_SPACE_ID="$PRODUCTION_SPACE_ID" \
515
+ STORYPANE_MANAGEMENT_TOKEN="$STORYPANE_PRODUCTION_MANAGEMENT_TOKEN" \
516
+ storypane migrations pull --out storypane/production-migrations
517
+ # Substitute the latest numbered production JSON file returned by this pull.
518
+ storypane generate-types --input storypane/production-migrations/3.json \
519
+ --out src/types/storypane.ts
520
+ ```
521
+
522
+ ### CI/CD
523
+
524
+ Use separate stages for dev validation and production promotion. The authoring
525
+ stage targets dev with its deploy-capable token:
526
+
527
+ ```sh
528
+ storypane migrations push --deploy --yes
529
+ storypane migrations pull
530
+ storypane generate-types --out src/types/storypane.ts
531
+ # Run your application's build/tests against dev.
532
+ ```
533
+
534
+ After review, a release stage keeps `STORYPANE_SPACE_ID` pointed at dev and adds
535
+ `STORYPANE_PRODUCTION_MANAGEMENT_TOKEN` from the CI secret store:
536
+
537
+ ```sh
538
+ storypane migrations status
539
+ storypane migrations promote --yes
540
+ ```
541
+
542
+ If production has advanced, sync and resolve conflicts before this stage. Generate
543
+ production types using the separate pull shown above, then build/deploy the live
544
+ frontend with production's delivery configuration. Direct `push --deploy` to a
545
+ read-only production space is rejected.
546
+
547
+ `--yes` explicitly accepts the reviewed data loss for noninteractive deployment;
548
+ validation and conflict checks still
549
+ apply. A stale impact review or failed deployment exits nonzero. Retrying the
550
+ same migration is safe, including after a lost response. The CLI does not commit
551
+ files or deploy your application. A successful `push --deploy` turns the local
552
+ active file into a numbered deployed snapshot. After promotion, pull again to
553
+ refresh dev's files before further edits.
554
+
555
+ ### Runtime version warnings
556
+
557
+ Codegen exports `storypaneMigrationVersion` with the block types. Pass that
558
+ constant as `migrationVersion` to `createStorypaneClient`, `connectPreview`, and
559
+ `useStorypaneStory`. Published responses and preview updates include their actual
560
+ version. A mismatch logs one warning per observed version per client/connection;
561
+ **content is still returned and preview remains connected**. Unversioned clients
562
+ also keep working. Existing caching options remain available; cached/static
563
+ content has no separate freshness check.
564
+
565
+ Warnings do not adapt newer content to older application code. Removed/reset
566
+ fields can require renderer changes even though requests succeed. Types describe
567
+ the locally pulled deployment; draft values can be incomplete. Rebuild static
568
+ sites after publishing or deploying schemas.
569
+
570
+ ### Existing schema-pull commands
571
+
572
+ The original commands still work with `STORYPANE_PREVIEW_TOKEN`:
573
+
574
+ ```sh
575
+ storypane pull-components --out storypane/components.json
576
+ storypane generate-types --input storypane/components.json --out src/types/storypane.ts
577
+ ```
578
+
579
+ The schema endpoint returns only the latest deployed metadata, without content,
580
+ defaults, or source URLs. A published-only token cannot read schemas. Codegen is
581
+ offline and writes atomically; invalid input preserves the previous output.
582
+ Both workflows require the initial schema migration to be deployed first.
583
+
584
+ ## Media
585
+
586
+ `client.getAsset(assetId)` returns a `Response` with the original asset bytes for
587
+ server-side rendering or static export. It requires a valid delivery token and
588
+ only serves assets referenced by a current publication in that token's space;
589
+ draft-only and historical references do not grant access. Keep the token on the
590
+ server. Static copies must be rebuilt or removed when their publications change.
591
+
592
+ ### Public media URLs
593
+
594
+ An administrator can enable **API & preview → Public media delivery** for a space.
595
+ It is disabled by default. Only assets referenced by an active current publication
596
+ become public; draft-only and historical references do not qualify. Archiving an
597
+ asset preserves its live references and original bytes.
598
+
599
+ `publicAssetUrl` is browser-safe and does not construct a client, make a request,
600
+ or accept a delivery token. For example, in a React renderer:
601
+
602
+ ```tsx
603
+ import { publicAssetUrl, type PublicImageOptions } from '@storypane/sdk';
604
+
605
+ const source = { url: 'https://cms.example.com', spaceId, assetId: hero.image.assetId };
606
+ const image = (width: PublicImageOptions['width']) =>
607
+ publicAssetUrl(source, { width, aspect: '16:9' });
608
+
609
+ <img
610
+ src={image(640)}
611
+ srcSet={`${image(320)} 320w, ${image(640)} 640w, ${image(1280)} 1280w`}
612
+ sizes="(max-width: 640px) 100vw, 50vw"
613
+ width={1280}
614
+ height={720}
615
+ alt={hero.image.alt ?? ''}
616
+ />;
617
+
618
+ const originalDownload = publicAssetUrl(source);
619
+ ```
620
+
621
+ Widths are limited to 160, 320, 640, 1280, 1920 and 2560. Optional aspect ratios are
622
+ `original`, `1:1`, `4:3`, `3:4`, `16:9` and `9:16`. Explicit ratios support `cover`
623
+ (default) or `contain`; original aspect uses scale-down. Output formats are `webp`
624
+ (default), `jpeg` and `png`. Lossy output accepts quality 60, 80 (default) or 90;
625
+ PNG rejects a quality option. The helper normalizes defaults and rejects unsupported
626
+ options. Originals support HEAD, ETags, and a single byte range. Variants support
627
+ HEAD and ETags; they do not expose partial image ranges.
628
+
629
+ Cover crops use the asset's saved focal point, defaulting to center. Saving focal
630
+ metadata changes every public crop of that asset immediately; it does not wait for
631
+ story publication. The `asset.updated` webhook includes the metadata revision so
632
+ static exporters can rebuild. Transformed animations use a still frame; originals
633
+ preserve animation. Images larger than 20 MiB, unsupported inputs, or unavailable
634
+ image processing return an error rather than substituting the original.
635
+
636
+ Public responses use `Cache-Control: no-store`. The CMS checks live eligibility
637
+ before serving each request. The Node runtime computes image variants on demand.
638
+ Disabling the setting or removing the last live reference returns 404 on the next
639
+ request, including cached and conditional requests.
640
+ Previously downloaded files and static copies cannot be recalled. Keep the existing
641
+ authenticated `getAsset` integration for static export when public media is disabled.
642
+
643
+ ## MCP
644
+
645
+ Run `storypane mcp` (Node 20+) with `STORYPANE_URL`, `STORYPANE_SPACE_ID`, and a
646
+ private `STORYPANE_MANAGEMENT_TOKEN`. The stdio adapter exposes read and write
647
+ tools over the management API; the same scopes, issuer permissions and revocation
648
+ apply. See [MCP configuration and contracts](https://github.com/lkuich/storypane/blob/main/docs/management-api.md#mcp-adapter).