@storystick/next 0.0.0-stage → 0.2.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +112 -0
  2. package/PUBLIC-API.md +31 -0
  3. package/README.md +522 -2
  4. package/dist/.source-hash +1 -0
  5. package/dist/cache-tags.d.ts +2 -0
  6. package/dist/cache-tags.js +12 -0
  7. package/dist/clean.d.ts +1 -0
  8. package/dist/clean.js +1 -0
  9. package/dist/code-version.d.ts +29 -0
  10. package/dist/code-version.js +48 -0
  11. package/dist/file.d.ts +1 -0
  12. package/dist/file.js +1 -0
  13. package/dist/i18n-seo.d.ts +1 -0
  14. package/dist/i18n-seo.js +1 -0
  15. package/dist/image.d.ts +11 -0
  16. package/dist/image.js +22 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +3 -0
  19. package/dist/index.react-server.d.ts +3 -0
  20. package/dist/index.react-server.js +12 -0
  21. package/dist/link.d.ts +10 -0
  22. package/dist/link.js +8 -0
  23. package/dist/preview/index.d.ts +29 -0
  24. package/dist/preview/index.js +24 -0
  25. package/dist/preview/internal.d.ts +61 -0
  26. package/dist/preview/internal.js +97 -0
  27. package/dist/preview/preview-request-identity.d.ts +4 -0
  28. package/dist/preview/preview-request-identity.js +63 -0
  29. package/dist/preview/provenance-config.d.ts +42 -0
  30. package/dist/preview/provenance-config.js +132 -0
  31. package/dist/preview/provenance-frame-builder.d.ts +6 -0
  32. package/dist/preview/provenance-frame-builder.js +10 -0
  33. package/dist/preview/provenance-response.d.ts +5 -0
  34. package/dist/preview/provenance-response.js +9 -0
  35. package/dist/preview/provenance-runtime-adapter.d.ts +9 -0
  36. package/dist/preview/provenance-runtime-adapter.js +42 -0
  37. package/dist/preview/render-frame-provenance.d.ts +23 -0
  38. package/dist/preview/render-frame-provenance.js +47 -0
  39. package/dist/preview/route-authoring.d.ts +3 -0
  40. package/dist/preview/route-authoring.js +80 -0
  41. package/dist/preview/route-context.d.ts +3 -0
  42. package/dist/preview/route-context.js +38 -0
  43. package/dist/preview/route-disable.d.ts +7 -0
  44. package/dist/preview/route-disable.js +63 -0
  45. package/dist/preview/route-document-provenance.d.ts +23 -0
  46. package/dist/preview/route-document-provenance.js +46 -0
  47. package/dist/preview/route-errors.d.ts +1 -0
  48. package/dist/preview/route-errors.js +1 -0
  49. package/dist/preview/route-handler.d.ts +36 -0
  50. package/dist/preview/route-handler.js +139 -0
  51. package/dist/preview/route-initial.d.ts +9 -0
  52. package/dist/preview/route-initial.js +156 -0
  53. package/dist/preview/route-provenance-runtime.d.ts +34 -0
  54. package/dist/preview/route-provenance-runtime.js +90 -0
  55. package/dist/preview/route-token.d.ts +6 -0
  56. package/dist/preview/route-token.js +46 -0
  57. package/dist/preview/server-preview-internals.d.ts +3 -0
  58. package/dist/preview/server-preview-internals.js +3 -0
  59. package/dist/preview/slice-locale.d.ts +5 -0
  60. package/dist/preview/slice-locale.js +15 -0
  61. package/dist/preview-error-surface.d.ts +7 -0
  62. package/dist/preview-error-surface.js +20 -0
  63. package/dist/preview-exit.d.ts +7 -0
  64. package/dist/preview-exit.js +71 -0
  65. package/dist/preview-metadata.d.ts +15 -0
  66. package/dist/preview-metadata.js +13 -0
  67. package/dist/preview-mode-latch.d.ts +133 -0
  68. package/dist/preview-mode-latch.js +231 -0
  69. package/dist/preview-nav-location.d.ts +12 -0
  70. package/dist/preview-nav-location.js +149 -0
  71. package/dist/preview-nav.d.ts +94 -0
  72. package/dist/preview-nav.js +523 -0
  73. package/dist/preview-refresh-coordinator.d.ts +26 -0
  74. package/dist/preview-refresh-coordinator.js +93 -0
  75. package/dist/preview-render-error.d.ts +11 -0
  76. package/dist/preview-render-error.js +36 -0
  77. package/dist/preview-render-recovery.d.ts +9 -0
  78. package/dist/preview-render-recovery.js +42 -0
  79. package/dist/preview-session-recovery.d.ts +1 -0
  80. package/dist/preview-session-recovery.js +1 -0
  81. package/dist/preview-standalone-host.d.ts +3 -0
  82. package/dist/preview-standalone-host.js +55 -0
  83. package/dist/preview-text.d.ts +2 -0
  84. package/dist/preview-text.js +1 -0
  85. package/dist/preview-view-bootstrap.d.ts +5 -0
  86. package/dist/preview-view-bootstrap.js +13 -0
  87. package/dist/public-delivery.d.ts +6 -0
  88. package/dist/public-delivery.js +7 -0
  89. package/dist/query-fetch-options.d.ts +7 -0
  90. package/dist/query-fetch-options.js +193 -0
  91. package/dist/query.d.ts +2 -0
  92. package/dist/query.js +1 -0
  93. package/dist/rendering.d.ts +33 -0
  94. package/dist/rendering.js +42 -0
  95. package/dist/revalidate.d.ts +12 -0
  96. package/dist/revalidate.js +33 -0
  97. package/dist/rich-text.d.ts +2 -0
  98. package/dist/rich-text.js +1 -0
  99. package/dist/server-runtime.d.ts +208 -0
  100. package/dist/server-runtime.js +1584 -0
  101. package/dist/server.d.ts +8 -0
  102. package/dist/server.js +6 -0
  103. package/dist/slice-zone.d.ts +2 -0
  104. package/dist/slice-zone.js +1 -0
  105. package/dist/storystick-component-types.d.ts +26 -0
  106. package/dist/storystick-component-types.js +1 -0
  107. package/dist/storystick-component.d.ts +10 -0
  108. package/dist/storystick-component.js +45 -0
  109. package/dist/storystick-error-recovery.d.ts +12 -0
  110. package/dist/storystick-error-recovery.js +11 -0
  111. package/dist/storystick-preview-runtime.d.ts +30 -0
  112. package/dist/storystick-preview-runtime.js +561 -0
  113. package/dist/storystick.d.ts +24 -0
  114. package/dist/storystick.js +23 -0
  115. package/dist/testing.d.ts +9 -0
  116. package/dist/testing.js +40 -0
  117. package/dist/video.d.ts +1 -0
  118. package/dist/video.js +1 -0
  119. package/package.json +128 -3
  120. package/recipes/README.md +91 -0
  121. package/recipes/resource-mirror.ts +49 -0
@@ -0,0 +1,6 @@
1
+ import 'server-only';
2
+ import { type PreviewProvenanceRuntime } from './provenance-runtime-adapter.js';
3
+ import { type BuildPreviewProvenanceFrameOptions, type BuildPreviewProvenanceFrameResult } from '@storystick/preview/server/provenance-frame-builder';
4
+ export * from '@storystick/preview/server/provenance-frame-builder';
5
+ export declare function buildPreviewProvenanceFrame(options: BuildPreviewProvenanceFrameOptions): Promise<BuildPreviewProvenanceFrameResult | null>;
6
+ export declare function buildPreviewProvenanceFrameWithRuntime(options: BuildPreviewProvenanceFrameOptions, injectedRuntime?: PreviewProvenanceRuntime): Promise<BuildPreviewProvenanceFrameResult | null>;
@@ -0,0 +1,10 @@
1
+ import 'server-only';
2
+ import { resolvePreviewProvenanceRuntime } from './provenance-runtime-adapter.js';
3
+ import { buildPreviewProvenanceFrameWithRuntime as sharedBuildProvenance } from '@storystick/preview/server/provenance-frame-builder';
4
+ export * from '@storystick/preview/server/provenance-frame-builder';
5
+ export async function buildPreviewProvenanceFrame(options) {
6
+ return buildPreviewProvenanceFrameWithRuntime(options);
7
+ }
8
+ export async function buildPreviewProvenanceFrameWithRuntime(options, injectedRuntime) {
9
+ return sharedBuildProvenance(options, resolvePreviewProvenanceRuntime(injectedRuntime));
10
+ }
@@ -0,0 +1,5 @@
1
+ import 'server-only';
2
+ export interface MarkStorystickPreviewProvenanceNoStoreOptions {
3
+ readonly enabled?: boolean;
4
+ }
5
+ export declare function markStorystickPreviewProvenanceNoStore(options?: MarkStorystickPreviewProvenanceNoStoreOptions): void;
@@ -0,0 +1,9 @@
1
+ import 'server-only';
2
+ import { unstable_noStore } from 'next/cache';
3
+ import { isStorystickPreviewProvenanceEnabled } from '../preview-metadata.js';
4
+ export function markStorystickPreviewProvenanceNoStore(options = {}) {
5
+ const enabled = options.enabled ?? isStorystickPreviewProvenanceEnabled();
6
+ if (!enabled)
7
+ return;
8
+ unstable_noStore();
9
+ }
@@ -0,0 +1,9 @@
1
+ import 'server-only';
2
+ import type { QuerySchemaDefinition } from '@storystick/client';
3
+ import type { PreviewProvenanceEditableTextPathManifest } from '@storystick/preview/provenance';
4
+ export type * from '@storystick/preview/server/provenance-runtime';
5
+ export { createPreviewProvenanceTemporalIdentity, bindPreviewRuntimeToReads, previewRuntimeForReads, bindPreviewRuntimeToSidecar, previewRuntimeForSidecar, previewRuntimeForSchema } from '@storystick/preview/server/provenance-runtime';
6
+ import { type PreviewProvenanceRuntime } from '@storystick/preview/server/provenance-runtime';
7
+ export declare const defaultPreviewProvenanceRuntime: PreviewProvenanceRuntime;
8
+ export declare function resolvePreviewProvenanceRuntime(runtime: PreviewProvenanceRuntime | undefined): PreviewProvenanceRuntime;
9
+ export declare function bindSiteQuerySchema<TSchema extends QuerySchemaDefinition>(schema: TSchema, manifest: PreviewProvenanceEditableTextPathManifest): TSchema;
@@ -0,0 +1,42 @@
1
+ import 'server-only';
2
+ import { resolveStorystickServerCodeVersion, } from '../code-version.js';
3
+ import { readPreviewProvenanceSessionId, resolvePreviewProvenanceRouteIdentity, } from './preview-request-identity.js';
4
+ import { unstable_resolveStorystickPreviewProvenanceConfig, } from './provenance-config.js';
5
+ import { markStorystickPreviewProvenanceNoStore } from './provenance-response.js';
6
+ export { createPreviewProvenanceTemporalIdentity, bindPreviewRuntimeToReads, previewRuntimeForReads, bindPreviewRuntimeToSidecar, previewRuntimeForSidecar, previewRuntimeForSchema } from '@storystick/preview/server/provenance-runtime';
7
+ import { bindPreviewRuntimeToSchema, createDefaultPreviewProvenanceTemporalIdentity } from '@storystick/preview/server/provenance-runtime';
8
+ export const defaultPreviewProvenanceRuntime = {
9
+ config: {
10
+ isEnabled: () => process.env.STORYSTICK_PREVIEW_PROVENANCE === '1',
11
+ isProduction: () => process.env.NODE_ENV === 'production',
12
+ resolveCodeVersion: (options = {}) => resolveStorystickServerCodeVersion(options),
13
+ resolveSemanticConfig: () => unstable_resolveStorystickPreviewProvenanceConfig(),
14
+ resolveSemanticManifest: async () => ({}),
15
+ },
16
+ recorder: {
17
+ markNoStore: markStorystickPreviewProvenanceNoStore,
18
+ readSessionId: readPreviewProvenanceSessionId,
19
+ resolveRouteIdentity: resolvePreviewProvenanceRouteIdentity,
20
+ },
21
+ transport: {
22
+ createTemporalIdentity: createDefaultPreviewProvenanceTemporalIdentity,
23
+ },
24
+ };
25
+ export function resolvePreviewProvenanceRuntime(runtime) {
26
+ return runtime ?? defaultPreviewProvenanceRuntime;
27
+ }
28
+ export function bindSiteQuerySchema(schema, manifest) {
29
+ const boundSchema = Object.freeze({ ...schema });
30
+ const runtime = {
31
+ ...defaultPreviewProvenanceRuntime,
32
+ config: {
33
+ ...defaultPreviewProvenanceRuntime.config,
34
+ isEnabled: () => true,
35
+ resolveSemanticConfig: () => ({ enabled: true, fixtureEnabled: false, sessionId: '',
36
+ codeVersion: resolveStorystickServerCodeVersion(), fixture: null }),
37
+ resolveSemanticManifest: async () => manifest,
38
+ },
39
+ };
40
+ bindPreviewRuntimeToSchema(boundSchema, runtime);
41
+ return boundSchema;
42
+ }
@@ -0,0 +1,23 @@
1
+ import 'server-only';
2
+ import { type ReactElement, type ReactNode } from 'react';
3
+ import { type PreviewProvenanceRuntime } from './provenance-runtime-adapter.js';
4
+ import { type PreviewRenderFrame, type BuildPreviewRenderFrameOptions, type BuildPreviewDocumentRenderFrameFromReadsOptions, type PreviewDocumentRenderFrame } from '@storystick/preview/server/render-frame-provenance';
5
+ export * from '@storystick/preview/server/render-frame-provenance';
6
+ export interface unstable_StorystickRenderFrameProps {
7
+ readonly frame: Pick<PreviewRenderFrame, 'frameId' | 'sessionId' | 'publicSidecar'> | null | undefined;
8
+ readonly attrs?: Readonly<Record<string, string>>;
9
+ readonly children: ReactNode;
10
+ }
11
+ export interface RenderPreviewScopeFromReadsOptions<TContext = unknown> extends Omit<BuildPreviewDocumentRenderFrameFromReadsOptions<TContext>, 'frameId' | 'routeHash'> {
12
+ readonly frameId?: string;
13
+ readonly routeHash?: string;
14
+ readonly attrs?: Readonly<Record<string, string>>;
15
+ readonly render: (context: TContext) => ReactNode | Promise<ReactNode>;
16
+ }
17
+ export declare function unstable_StorystickRenderFrame({ frame, attrs, children, }: unstable_StorystickRenderFrameProps): ReactElement;
18
+ export declare function unstable_renderPreviewScopeFromReads<TContext = unknown>(options: RenderPreviewScopeFromReadsOptions<TContext>): Promise<ReactElement>;
19
+ export declare function unstable_renderPreviewScopeFromReadsWithRuntime<TContext = unknown>(options: RenderPreviewScopeFromReadsOptions<TContext>, injectedRuntime?: PreviewProvenanceRuntime): Promise<ReactElement>;
20
+ export declare function unstable_buildPreviewRenderFrame<TDocument = unknown, TContext = unknown>(options: BuildPreviewRenderFrameOptions<TDocument, TContext>): Promise<PreviewRenderFrame<TDocument, TContext> | null>;
21
+ export declare function unstable_buildPreviewRenderFrameWithRuntime<TDocument = unknown, TContext = unknown>(options: BuildPreviewRenderFrameOptions<TDocument, TContext>, injectedRuntime?: PreviewProvenanceRuntime): Promise<PreviewRenderFrame<TDocument, TContext> | null>;
22
+ export declare function unstable_buildPreviewDocumentRenderFrameFromReads<TContext = unknown>(options: BuildPreviewDocumentRenderFrameFromReadsOptions<TContext>): Promise<PreviewDocumentRenderFrame<TContext> | null>;
23
+ export declare function unstable_buildPreviewDocumentRenderFrameFromReadsWithRuntime<TContext = unknown>(options: BuildPreviewDocumentRenderFrameFromReadsOptions<TContext>, injectedRuntime?: PreviewProvenanceRuntime): Promise<PreviewDocumentRenderFrame<TContext> | null>;
@@ -0,0 +1,47 @@
1
+ import 'server-only';
2
+ import { createElement, Fragment } from 'react';
3
+ import { PreviewProvenanceSidecar } from '@storystick/react/internal/preview-provenance';
4
+ import { createPreviewOccurrenceId } from '@storystick/preview/server/render-id';
5
+ import { resolvePreviewProvenanceRuntime } from './provenance-runtime-adapter.js';
6
+ import { unstable_buildPreviewRenderFrameWithRuntime as sharedBuildFrame, unstable_buildPreviewDocumentRenderFrameFromReadsWithRuntime as sharedBuildReads } from '@storystick/preview/server/render-frame-provenance';
7
+ export * from '@storystick/preview/server/render-frame-provenance';
8
+ export function unstable_StorystickRenderFrame({ frame, attrs, children, }) {
9
+ const content = attrs === undefined
10
+ ? children
11
+ : createElement('div', attrs, children);
12
+ return createElement(Fragment, null, frame
13
+ ? createElement(PreviewProvenanceSidecar, {
14
+ frameId: frame.frameId,
15
+ sessionId: frame.sessionId,
16
+ sidecar: frame.publicSidecar,
17
+ })
18
+ : null, content);
19
+ }
20
+ export async function unstable_renderPreviewScopeFromReads(options) {
21
+ return unstable_renderPreviewScopeFromReadsWithRuntime(options);
22
+ }
23
+ export async function unstable_renderPreviewScopeFromReadsWithRuntime(options, injectedRuntime) {
24
+ const runtime = resolvePreviewProvenanceRuntime(injectedRuntime);
25
+ const { attrs, render, ...frameOptions } = options;
26
+ const frameId = frameOptions.frameId ?? createPreviewOccurrenceId();
27
+ const frame = await unstable_buildPreviewDocumentRenderFrameFromReadsWithRuntime({
28
+ ...frameOptions,
29
+ frameId,
30
+ routeHash: frameOptions.routeHash ?? `occurrence:${frameId}`,
31
+ }, runtime);
32
+ const context = (frame?.codedContext ?? frameOptions.context);
33
+ const children = await render(context);
34
+ return unstable_StorystickRenderFrame({ frame, attrs, children });
35
+ }
36
+ export async function unstable_buildPreviewRenderFrame(options) {
37
+ return unstable_buildPreviewRenderFrameWithRuntime(options);
38
+ }
39
+ export async function unstable_buildPreviewRenderFrameWithRuntime(options, injectedRuntime) {
40
+ return sharedBuildFrame(options, resolvePreviewProvenanceRuntime(injectedRuntime));
41
+ }
42
+ export async function unstable_buildPreviewDocumentRenderFrameFromReads(options) {
43
+ return unstable_buildPreviewDocumentRenderFrameFromReadsWithRuntime(options);
44
+ }
45
+ export async function unstable_buildPreviewDocumentRenderFrameFromReadsWithRuntime(options, injectedRuntime) {
46
+ return sharedBuildReads(options, resolvePreviewProvenanceRuntime(injectedRuntime));
47
+ }
@@ -0,0 +1,3 @@
1
+ /** Same-origin elevation from an active PreviewGrant to a bounded write admission. */
2
+ import type { NextRequest } from 'next/server';
3
+ export declare function createStorystickPreviewAuthoringHandler(): (request: NextRequest) => Promise<Response>;
@@ -0,0 +1,80 @@
1
+ /** Same-origin elevation from an active PreviewGrant to a bounded write admission. */
2
+ import { NextResponse } from 'next/server';
3
+ import { cookies } from 'next/headers';
4
+ import { PreviewGrantExchangeError, classifyPreviewGrantError, exchangePreviewAuthoringAdmissionForServer, } from '@storystick/preview/server/grant';
5
+ import { resolveStorystickApplicationBuildId } from '../code-version.js';
6
+ import { getPreviewTransport, resolvePreviewRequestOrigin } from './internal.js';
7
+ import { readPreviewGrantCookie } from '@storystick/preview/server/extract-token';
8
+ import { previewControlErrorResponse, previewErrorResponse, } from './route-errors.js';
9
+ export function createStorystickPreviewAuthoringHandler() {
10
+ return async function POST(request) {
11
+ const requestOrigin = resolvePreviewRequestOrigin(request);
12
+ if (request.headers.get('origin') !== requestOrigin) {
13
+ return previewErrorResponse(403, 'PREVIEW_ORIGIN_MISMATCH', 'A same-origin Preview request is required.', { category: 'auth', recoverable: true });
14
+ }
15
+ let renewal;
16
+ try {
17
+ renewal = parseRenewal(await request.json());
18
+ }
19
+ catch {
20
+ return previewErrorResponse(400, 'PREVIEW_AUTHORING_REQUEST_INVALID', 'The authoring request is invalid.', { category: 'validation' });
21
+ }
22
+ try {
23
+ const transport = await getPreviewTransport();
24
+ const scope = { ...transport, requestOrigin };
25
+ const admission = await readPreviewGrantCookie(await cookies(), scope);
26
+ if (admission === null) {
27
+ return previewErrorResponse(401, 'PREVIEW_CONTEXT_ENDED', 'Preview has been closed.', { category: 'auth', recoverable: true });
28
+ }
29
+ if (!admission) {
30
+ return previewErrorResponse(401, 'PREVIEW_GRANT_MISSING', 'Open Preview from Studio again.', { category: 'auth', recoverable: true });
31
+ }
32
+ const applicationBuildId = resolveStorystickApplicationBuildId();
33
+ if (admission.applicationBuildId !== applicationBuildId) {
34
+ return previewErrorResponse(409, 'CONTENT_SHAPE_RELOAD_REQUIRED', 'Reopen Preview from Studio after the site deployment changed.', { category: 'conflict', recoverable: true });
35
+ }
36
+ const result = await exchangePreviewAuthoringAdmissionForServer({
37
+ endpoint: transport.endpoint,
38
+ apiKey: transport.apiKey,
39
+ applicationBuildId,
40
+ grantToken: admission.grantToken,
41
+ requestOrigin,
42
+ contentShapeCapability: admission.contentShapeCapability,
43
+ ...renewal,
44
+ });
45
+ if (result.projectShortId !== transport.projectId) {
46
+ return previewErrorResponse(403, 'PREVIEW_PROJECT_MISMATCH', 'This site is configured for a different Storystick project.', { category: 'auth', recoverable: true });
47
+ }
48
+ return NextResponse.json(result, {
49
+ headers: {
50
+ 'Cache-Control': 'private, no-store',
51
+ 'X-Content-Type-Options': 'nosniff',
52
+ },
53
+ });
54
+ }
55
+ catch (error) {
56
+ if (error instanceof PreviewGrantExchangeError) {
57
+ return previewErrorResponse(classifyPreviewGrantError(error, 'authoring'));
58
+ }
59
+ return previewControlErrorResponse(error);
60
+ }
61
+ };
62
+ }
63
+ function parseRenewal(value) {
64
+ if (!value || typeof value !== 'object' || Array.isArray(value))
65
+ throw new Error();
66
+ const body = value;
67
+ const admissionId = optionalId(body.admissionId);
68
+ const fileDelegationId = optionalId(body.fileDelegationId);
69
+ if (Boolean(admissionId) !== Boolean(fileDelegationId))
70
+ throw new Error();
71
+ return admissionId ? { admissionId, fileDelegationId } : {};
72
+ }
73
+ function optionalId(value) {
74
+ if (value === undefined)
75
+ return undefined;
76
+ if (typeof value !== 'string' || value.length === 0 || value.length > 256) {
77
+ throw new Error();
78
+ }
79
+ return value;
80
+ }
@@ -0,0 +1,3 @@
1
+ /** Shared Preview commands stay inside the existing customer-site dispatcher. */
2
+ import type { NextRequest } from 'next/server';
3
+ export declare function handlePreviewContext(request: NextRequest): Promise<Response>;
@@ -0,0 +1,38 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { cookies } from 'next/headers';
3
+ import { createPreviewControlClient, getPreviewTransport, resolvePreviewRequestOrigin } from './internal.js';
4
+ import { resolvePreviewGrantAccess } from '@storystick/preview/server/extract-token';
5
+ import { resolveStorystickApplicationBuildId } from '../code-version.js';
6
+ import { readPreviewContextCommand } from '@storystick/preview/server/read-context-command';
7
+ import { previewControlErrorResponse, previewErrorResponse } from './route-errors.js';
8
+ export async function handlePreviewContext(request) {
9
+ const requestOrigin = resolvePreviewRequestOrigin(request);
10
+ if (request.method === 'POST' && request.headers.get('origin') !== requestOrigin) {
11
+ return previewErrorResponse(403, 'PREVIEW_ORIGIN_MISMATCH', 'A same-origin Preview command is required.');
12
+ }
13
+ try {
14
+ const transport = await getPreviewTransport();
15
+ const fields = await resolvePreviewGrantAccess(await cookies(), { ...transport, requestOrigin, applicationBuildId: resolveStorystickApplicationBuildId() });
16
+ if (!fields)
17
+ return previewErrorResponse(401, 'PREVIEW_GRANT_MISSING', 'Preview is not active.');
18
+ const client = await createPreviewControlClient(fields.token);
19
+ if (request.method === 'GET') {
20
+ const result = await client.context();
21
+ return NextResponse.json(result, { headers: { 'Cache-Control': 'private, no-store' } });
22
+ }
23
+ const command = await readPreviewContextCommand(request);
24
+ if (!command)
25
+ return previewErrorResponse(400, 'PREVIEW_CONTEXT_INVALID', 'Invalid Preview command.');
26
+ const result = await client.changeContext({
27
+ commandId: command.commandId,
28
+ expectedContextId: command.expectedContextId,
29
+ expectedRevision: command.expectedRevision,
30
+ perspective: command.perspective,
31
+ });
32
+ return NextResponse.json(result, { status: result.status === 'conflict' ? 409 : 200,
33
+ headers: { 'Cache-Control': 'private, no-store' } });
34
+ }
35
+ catch (error) {
36
+ return previewControlErrorResponse(error);
37
+ }
38
+ }
@@ -0,0 +1,7 @@
1
+ /** Leave this navigation experience; other views retain their admission. */
2
+ import type { NextRequest } from 'next/server';
3
+ export type PreviewDisableHandlerOptions = {
4
+ redirectAfter?: string;
5
+ basePath?: string;
6
+ };
7
+ export declare function createStorystickPreviewDisableHandler(options?: PreviewDisableHandlerOptions): (request: NextRequest) => Promise<Response>;
@@ -0,0 +1,63 @@
1
+ /** Leave this navigation experience; other views retain their admission. */
2
+ import { NextResponse } from 'next/server';
3
+ import { cookies } from 'next/headers';
4
+ import { StorystickError } from '@storystick/client';
5
+ import { PreviewRedirectError, assertSafePreviewRedirect, } from '@storystick/preview/server/grant';
6
+ import { createPreviewControlClient, getPreviewTransport, resolvePreviewRequestOrigin } from './internal.js';
7
+ import { resolvePreviewGrantAccess } from '@storystick/preview/server/extract-token';
8
+ import { resolveStorystickApplicationBuildId } from '../code-version.js';
9
+ import { readPreviewContextCommand } from '@storystick/preview/server/read-context-command';
10
+ import { previewControlErrorResponse, previewErrorResponse } from './route-errors.js';
11
+ export function createStorystickPreviewDisableHandler(options = {}) {
12
+ const basePath = options.basePath ?? '';
13
+ const rawRedirect = options.redirectAfter ?? (basePath || '/');
14
+ let safeRedirect;
15
+ try {
16
+ safeRedirect = assertSafePreviewRedirect(rawRedirect, basePath);
17
+ }
18
+ catch (error) {
19
+ if (error instanceof PreviewRedirectError) {
20
+ throw new Error('[storystick/preview] invalid preview disable redirectAfter');
21
+ }
22
+ throw error;
23
+ }
24
+ return async function POST(request) {
25
+ const requestOrigin = resolvePreviewRequestOrigin(request);
26
+ if (request.headers.get('origin') !== requestOrigin) {
27
+ return previewErrorResponse(403, 'PREVIEW_ORIGIN_MISMATCH', 'A same-origin Preview command is required.');
28
+ }
29
+ const transport = await getPreviewTransport();
30
+ const reader = await cookies();
31
+ try {
32
+ const fields = await resolvePreviewGrantAccess(reader, { ...transport, requestOrigin, applicationBuildId: resolveStorystickApplicationBuildId() });
33
+ if (fields) {
34
+ const client = await createPreviewControlClient(fields.token);
35
+ const command = await readPreviewContextCommand(request);
36
+ // No observable context (expired/refused bootstrap): clear this tab only.
37
+ if (command) {
38
+ if (command.perspective !== null)
39
+ return previewErrorResponse(400, 'PREVIEW_CONTEXT_INVALID', 'Invalid Preview exit.');
40
+ const result = await client.changeContext({
41
+ ...command, perspective: null,
42
+ });
43
+ if (result.status === 'conflict')
44
+ return previewErrorResponse(409, 'PREVIEW_CONTEXT_CONFLICT', 'The Preview context changed. Retry leaving Preview.', { category: 'conflict', recoverable: true });
45
+ }
46
+ }
47
+ }
48
+ catch (error) {
49
+ const failure = previewControlErrorResponse(error);
50
+ // Explicit exit must also work after expiration or revocation. Invalid
51
+ // credentials cannot mutate server state, but can be removed locally.
52
+ const changedBuild = error instanceof StorystickError &&
53
+ error.code === 'storystick/CONTENT_SHAPE_RELOAD_REQUIRED';
54
+ if (failure.status !== 401 && failure.status !== 403 && !changedBuild)
55
+ return failure;
56
+ }
57
+ const response = request.headers.get('accept')?.includes('application/json')
58
+ ? NextResponse.json({ status: 'disabled' })
59
+ : NextResponse.redirect(new URL(safeRedirect, requestOrigin), { status: 302 });
60
+ response.headers.set('Cache-Control', 'no-store');
61
+ return response;
62
+ };
63
+ }
@@ -0,0 +1,23 @@
1
+ import 'server-only';
2
+ import { type ReactElement, type ReactNode } from 'react';
3
+ import type { PreviewProvenanceQueryResultLike } from '@storystick/preview/provenance';
4
+ import { type PreviewProvenanceRuntime } from './provenance-runtime-adapter.js';
5
+ import { type PreviewRouteDocumentFrame, type BuildPreviewRouteDocumentFrameOptions } from '@storystick/preview/server/route-document-provenance';
6
+ export * from '@storystick/preview/server/route-document-provenance';
7
+ export interface unstable_StorystickRoutePreviewFrameProps {
8
+ readonly frame: Pick<PreviewRouteDocumentFrame, 'frameId' | 'sessionId' | 'routeHash' | 'variantId' | 'publicSidecar'> | null | undefined;
9
+ readonly attrs?: Readonly<Record<string, string>>;
10
+ readonly children: ReactNode;
11
+ }
12
+ export interface RenderPreviewRouteDocumentOptions<TDocument extends PreviewProvenanceQueryResultLike, TContext = unknown> extends Omit<BuildPreviewRouteDocumentFrameOptions<TDocument, TContext>, 'routeHash'> {
13
+ readonly attrs?: Readonly<Record<string, string>>;
14
+ readonly render: (input: {
15
+ readonly document: TDocument;
16
+ readonly context: TContext | undefined;
17
+ }) => ReactNode | Promise<ReactNode>;
18
+ }
19
+ export declare function unstable_StorystickRoutePreviewFrame({ frame, attrs, children, }: unstable_StorystickRoutePreviewFrameProps): ReactElement;
20
+ export declare function unstable_renderPreviewRouteDocument<TDocument extends PreviewProvenanceQueryResultLike, TContext = unknown>(options: RenderPreviewRouteDocumentOptions<TDocument, TContext>): Promise<ReactElement>;
21
+ export declare function unstable_renderPreviewRouteDocumentWithRuntime<TDocument extends PreviewProvenanceQueryResultLike, TContext = unknown>(options: RenderPreviewRouteDocumentOptions<TDocument, TContext>, injectedRuntime?: PreviewProvenanceRuntime): Promise<ReactElement>;
22
+ export declare function unstable_buildPreviewRouteDocumentFrame<TDocument extends PreviewProvenanceQueryResultLike, TContext = unknown>(options: BuildPreviewRouteDocumentFrameOptions<TDocument, TContext>): Promise<PreviewRouteDocumentFrame<TDocument, TContext> | null>;
23
+ export declare function unstable_buildPreviewRouteDocumentFrameWithRuntime<TDocument extends PreviewProvenanceQueryResultLike, TContext = unknown>(options: BuildPreviewRouteDocumentFrameOptions<TDocument, TContext>, injectedRuntime?: PreviewProvenanceRuntime): Promise<PreviewRouteDocumentFrame<TDocument, TContext> | null>;
@@ -0,0 +1,46 @@
1
+ import 'server-only';
2
+ import { createElement, Fragment } from 'react';
3
+ import { PreviewProvenanceSidecar } from '@storystick/react/internal/preview-provenance';
4
+ import { resolvePreviewProvenanceRuntime, bindPreviewRuntimeToSidecar } from './provenance-runtime-adapter.js';
5
+ import { unstable_buildPreviewRouteDocumentFrameWithRuntime as sharedBuildRoute, unstable_attachPreviewRouteRenderContext, stripPreviewRouteMetadata } from '@storystick/preview/server/route-document-provenance';
6
+ export * from '@storystick/preview/server/route-document-provenance';
7
+ export function unstable_StorystickRoutePreviewFrame({ frame, attrs, children, }) {
8
+ const content = attrs !== undefined
9
+ ? createElement('div', attrs, children)
10
+ : children;
11
+ return createElement(Fragment, null, frame
12
+ ? createElement(PreviewProvenanceSidecar, {
13
+ frameId: frame.frameId,
14
+ sessionId: frame.sessionId,
15
+ sidecar: frame.publicSidecar,
16
+ })
17
+ : null, content);
18
+ }
19
+ export async function unstable_renderPreviewRouteDocument(options) {
20
+ return unstable_renderPreviewRouteDocumentWithRuntime(options);
21
+ }
22
+ export async function unstable_renderPreviewRouteDocumentWithRuntime(options, injectedRuntime) {
23
+ const runtime = resolvePreviewProvenanceRuntime(injectedRuntime);
24
+ const { attrs, render, ...frameOptions } = options;
25
+ const document = stripPreviewRouteMetadata(frameOptions.document);
26
+ const frame = await unstable_buildPreviewRouteDocumentFrameWithRuntime({
27
+ ...frameOptions,
28
+ document,
29
+ }, runtime);
30
+ if (frame)
31
+ bindPreviewRuntimeToSidecar(frame.publicSidecar, runtime);
32
+ const renderDocument = frame?.codedDocument
33
+ ? unstable_attachPreviewRouteRenderContext(frame.codedDocument, frame)
34
+ : document;
35
+ const context = frame
36
+ ? unstable_attachPreviewRouteRenderContext((frame.codedContext ?? frameOptions.context), frame)
37
+ : frameOptions.context;
38
+ const children = await render({ document: renderDocument, context });
39
+ return unstable_StorystickRoutePreviewFrame({ frame, attrs, children });
40
+ }
41
+ export async function unstable_buildPreviewRouteDocumentFrame(options) {
42
+ return unstable_buildPreviewRouteDocumentFrameWithRuntime(options);
43
+ }
44
+ export async function unstable_buildPreviewRouteDocumentFrameWithRuntime(options, injectedRuntime) {
45
+ return sharedBuildRoute(options, resolvePreviewProvenanceRuntime(injectedRuntime));
46
+ }
@@ -0,0 +1 @@
1
+ export * from '@storystick/preview/server/control-errors';
@@ -0,0 +1 @@
1
+ export * from '@storystick/preview/server/control-errors';
@@ -0,0 +1,36 @@
1
+ import type { NextRequest } from 'next/server';
2
+ import { type StorystickRevalidateHandlerOptions } from '../revalidate.js';
3
+ import { type PreviewDisableHandlerOptions } from './route-disable.js';
4
+ import { type PreviewHandlerOptions } from './route-initial.js';
5
+ import { type PreviewProvenanceRuntimeHandlerOptions } from './route-provenance-runtime.js';
6
+ import { type PreviewTokenHandlerOptions } from './route-token.js';
7
+ export interface RouteHandlerOptions {
8
+ /**
9
+ * Runtime URL prefix matching `next.config.js` `basePath`. Applied to the
10
+ * preview, token, and disable handlers unless overridden per handler.
11
+ */
12
+ basePath?: string;
13
+ preview?: PreviewHandlerOptions;
14
+ token?: PreviewTokenHandlerOptions;
15
+ disable?: PreviewDisableHandlerOptions;
16
+ provenanceRuntime?: PreviewProvenanceRuntimeHandlerOptions;
17
+ revalidate?: StorystickRevalidateHandlerOptions;
18
+ /** Optional application readback. Its source must be this deployed build. */
19
+ migrationReadback?: () => Promise<{
20
+ projectId: string;
21
+ artifactId: string;
22
+ artifactDigest: string;
23
+ schemaVersion: string | null;
24
+ querySchemaFingerprint: string;
25
+ publication?: {
26
+ version: 2;
27
+ rendering: 'request' | 'static' | 'unqualified';
28
+ receipts: boolean;
29
+ };
30
+ } | null>;
31
+ }
32
+ export interface RouteHandlers {
33
+ GET(request: NextRequest): Promise<Response>;
34
+ POST(request: NextRequest): Promise<Response>;
35
+ }
36
+ export declare function createRouteHandler(options?: RouteHandlerOptions): RouteHandlers;
@@ -0,0 +1,139 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { STORYSTICK_HEALTH_ROUTE_PATH, STORYSTICK_MIGRATION_READBACK_ROUTE_PATH, STORYSTICK_PREVIEW_DISABLE_ROUTE_PATH, STORYSTICK_PREVIEW_AUTHORING_ROUTE_PATH, STORYSTICK_PREVIEW_PROVENANCE_RUNTIME_ROUTE_PATH, STORYSTICK_PREVIEW_ROUTE_PATH, STORYSTICK_PREVIEW_TOKEN_ROUTE_PATH, STORYSTICK_PREVIEW_CONTEXT_ROUTE_PATH, STORYSTICK_REVALIDATE_ROUTE_PATH, } from '@storystick/preview/routes';
3
+ import { createStorystickRevalidateHandler, } from '../revalidate.js';
4
+ import { createStorystickPreviewDisableHandler, } from './route-disable.js';
5
+ import { createStorystickPreviewHandler, } from './route-initial.js';
6
+ import { createStorystickPreviewProvenanceRuntimeHandler, } from './route-provenance-runtime.js';
7
+ import { createStorystickPreviewTokenHandler, } from './route-token.js';
8
+ import { handlePreviewContext } from './route-context.js';
9
+ import { createStorystickPreviewAuthoringHandler } from './route-authoring.js';
10
+ export function createRouteHandler(options = {}) {
11
+ const preview = createStorystickPreviewHandler(withBasePath(options.preview, options.basePath));
12
+ const token = createStorystickPreviewTokenHandler(withBasePath(options.token, options.basePath));
13
+ const disable = createStorystickPreviewDisableHandler(withBasePath(options.disable, options.basePath));
14
+ const authoring = createStorystickPreviewAuthoringHandler();
15
+ const provenanceRuntime = createStorystickPreviewProvenanceRuntimeHandler(options.provenanceRuntime);
16
+ const revalidate = createStorystickRevalidateHandler(options.revalidate);
17
+ return {
18
+ async GET(request) {
19
+ switch (resolveAction(request, options.basePath)) {
20
+ case 'context': return handlePreviewContext(request);
21
+ case 'preview':
22
+ return preview(request);
23
+ case 'health':
24
+ return healthResponse();
25
+ case 'migration-readback':
26
+ return migrationReadbackResponse(options.migrationReadback);
27
+ case 'provenance-runtime':
28
+ return provenanceRuntime(request);
29
+ case 'token':
30
+ case 'disable':
31
+ case 'authoring':
32
+ case 'revalidate':
33
+ return methodNotAllowed('POST');
34
+ default:
35
+ return notFound();
36
+ }
37
+ },
38
+ async POST(request) {
39
+ switch (resolveAction(request, options.basePath)) {
40
+ case 'context': return handlePreviewContext(request);
41
+ case 'token':
42
+ return token(request);
43
+ case 'disable':
44
+ return disable(request);
45
+ case 'authoring':
46
+ return authoring(request);
47
+ case 'revalidate':
48
+ return revalidate(request);
49
+ case 'preview':
50
+ case 'health':
51
+ case 'migration-readback':
52
+ case 'provenance-runtime':
53
+ return methodNotAllowed('GET');
54
+ default:
55
+ return notFound();
56
+ }
57
+ },
58
+ };
59
+ }
60
+ function withBasePath(options, basePath) {
61
+ if (!basePath || options?.basePath)
62
+ return (options ?? {});
63
+ return { ...(options ?? {}), basePath };
64
+ }
65
+ function resolveAction(request, basePath) {
66
+ const pathname = stripBasePath(request.nextUrl.pathname, basePath);
67
+ switch (pathname) {
68
+ case STORYSTICK_PREVIEW_ROUTE_PATH:
69
+ return 'preview';
70
+ case STORYSTICK_PREVIEW_TOKEN_ROUTE_PATH:
71
+ return 'token';
72
+ case STORYSTICK_PREVIEW_CONTEXT_ROUTE_PATH:
73
+ return 'context';
74
+ case STORYSTICK_PREVIEW_DISABLE_ROUTE_PATH:
75
+ return 'disable';
76
+ case STORYSTICK_PREVIEW_AUTHORING_ROUTE_PATH:
77
+ return 'authoring';
78
+ case STORYSTICK_HEALTH_ROUTE_PATH:
79
+ return 'health';
80
+ case STORYSTICK_MIGRATION_READBACK_ROUTE_PATH:
81
+ return 'migration-readback';
82
+ case STORYSTICK_REVALIDATE_ROUTE_PATH:
83
+ return 'revalidate';
84
+ case STORYSTICK_PREVIEW_PROVENANCE_RUNTIME_ROUTE_PATH:
85
+ return 'provenance-runtime';
86
+ default:
87
+ return null;
88
+ }
89
+ }
90
+ function stripBasePath(pathname, basePath) {
91
+ if (!basePath)
92
+ return pathname;
93
+ const normalized = normalizeBasePath(basePath);
94
+ if (pathname === normalized)
95
+ return '/';
96
+ if (pathname.startsWith(`${normalized}/`)) {
97
+ return pathname.slice(normalized.length);
98
+ }
99
+ return pathname;
100
+ }
101
+ function normalizeBasePath(value) {
102
+ const trimmed = value.trim();
103
+ if (!trimmed || trimmed === '/')
104
+ return '';
105
+ const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
106
+ return withLeadingSlash.endsWith('/')
107
+ ? withLeadingSlash.slice(0, -1)
108
+ : withLeadingSlash;
109
+ }
110
+ function healthResponse() {
111
+ return NextResponse.json({ ok: true }, { headers: { 'Cache-Control': 'no-store' } });
112
+ }
113
+ async function migrationReadbackResponse(readback) {
114
+ const headers = { 'Cache-Control': 'no-store' };
115
+ if (!readback)
116
+ return notFound();
117
+ try {
118
+ const migration = await readback();
119
+ return NextResponse.json(migration ? { ok: true, migration } : { ok: false, error: 'migration_readback_not_configured' }, {
120
+ status: migration ? 200 : 503,
121
+ headers,
122
+ });
123
+ }
124
+ catch {
125
+ return NextResponse.json({ ok: false, error: 'migration_readback_unavailable' }, { status: 503, headers });
126
+ }
127
+ }
128
+ function methodNotAllowed(allow) {
129
+ return NextResponse.json({ ok: false, error: 'method_not_allowed' }, {
130
+ status: 405,
131
+ headers: {
132
+ Allow: allow,
133
+ 'Cache-Control': 'no-store',
134
+ },
135
+ });
136
+ }
137
+ function notFound() {
138
+ return NextResponse.json({ ok: false, error: 'not_found' }, { status: 404, headers: { 'Cache-Control': 'no-store' } });
139
+ }
@@ -0,0 +1,9 @@
1
+ /** One-shot PreviewGrant activation route. */
2
+ import type { NextRequest } from 'next/server';
3
+ export type PreviewHandlerOptions = {
4
+ /** Query param carrying the final same-origin path. Default `redirect`. */
5
+ redirectParam?: string;
6
+ /** Runtime URL prefix matching `next.config.js` `basePath`. */
7
+ basePath?: string;
8
+ };
9
+ export declare function createStorystickPreviewHandler(options?: PreviewHandlerOptions): (request: NextRequest) => Promise<Response>;